¿Alguien ha tenido éxito en las pruebas unitarias de procedimientos almacenados de SQL?

StackOverflow https://stackoverflow.com/questions/12374

  •  08-06-2019
  •  | 
  •  

Pregunta

Descubrimos que las pruebas unitarias que escribimos para nuestro código C#/C++ realmente dieron sus frutos.Pero todavía tenemos miles de líneas de lógica de negocios en procedimientos almacenados, que solo se prueban cuando nuestro producto se lanza a una gran cantidad de usuarios.

Lo que empeora esto es que algunos de estos procedimientos almacenados terminan siendo muy largos debido al impacto en el rendimiento al pasar tablas temporales entre SP.Esto nos ha impedido refactorizar para simplificar el código.

Hemos hecho varios intentos de crear pruebas unitarias en torno a algunos de nuestros procedimientos almacenados clave (principalmente probando el rendimiento), pero hemos descubierto que configurar los datos de prueba para estas pruebas es realmente difícil.Por ejemplo, terminamos copiando bases de datos de prueba.Además de esto, las pruebas terminan siendo muy sensibles al cambio, e incluso al cambio más pequeño en un proceso almacenado.o tabla requiere una gran cantidad de cambios en las pruebas.Entonces, después de que muchas compilaciones fallaron debido a que estas pruebas de bases de datos fallaron de manera intermitente, tuvimos que sacarlas del proceso de compilación.

Entonces, la parte principal de mis preguntas es:¿Alguien ha escrito alguna vez con éxito pruebas unitarias para sus procedimientos almacenados?

La segunda parte de mis preguntas es si las pruebas unitarias serían o son más fáciles con linq.

Estaba pensando que en lugar de tener que configurar tablas de datos de prueba, ¿podría simplemente crear una colección de objetos de prueba y probar su código linq en una situación de "linq a objetos"?(Soy totalmente nuevo en linq, así que no sé si esto funcionaría)

¿Fue útil?

Solución

Me encontré con el mismo problema hace un tiempo y descubrí que si creaba una clase base abstracta simple para el acceso a datos que me permitiera inyectar una conexión y una transacción, podía realizar una prueba unitaria de mis sprocs para ver si hacían el trabajo en SQL que yo les pedí que lo hicieran y luego retrocedieran para que ninguno de los datos de prueba quede en la base de datos.

Esto se sintió mejor que lo habitual "ejecutar un script para configurar mi base de datos de prueba, luego, después de ejecutar las pruebas, hacer una limpieza de los datos basura/de prueba".Esto también se sintió más cercano a las pruebas unitarias porque estas pruebas se podían ejecutar solas sin tener que tener una gran cantidad de "todo en la base de datos debe estar 'así' antes de ejecutar estas pruebas".

Aquí hay un fragmento de la clase base abstracta utilizada para el acceso a datos.

Public MustInherit Class Repository(Of T As Class)
    Implements IRepository(Of T)

    Private mConnectionString As String = ConfigurationManager.ConnectionStrings("Northwind.ConnectionString").ConnectionString
    Private mConnection As IDbConnection
    Private mTransaction As IDbTransaction

    Public Sub New()
        mConnection = Nothing
        mTransaction = Nothing
    End Sub

    Public Sub New(ByVal connection As IDbConnection, ByVal transaction As IDbTransaction)
        mConnection = connection
        mTransaction = transaction
    End Sub

    Public MustOverride Function BuildEntity(ByVal cmd As SqlCommand) As List(Of T)

    Public Function ExecuteReader(ByVal Parameter As Parameter) As List(Of T) Implements IRepository(Of T).ExecuteReader
        Dim entityList As List(Of T)
        If Not mConnection Is Nothing Then
            Using cmd As SqlCommand = mConnection.CreateCommand()
                cmd.Transaction = mTransaction
                cmd.CommandType = Parameter.Type
                cmd.CommandText = Parameter.Text
                If Not Parameter.Items Is Nothing Then
                    For Each param As SqlParameter In Parameter.Items
                        cmd.Parameters.Add(param)
                    Next
                End If
                entityList = BuildEntity(cmd)
                If Not entityList Is Nothing Then
                    Return entityList
                End If
            End Using
        Else
            Using conn As SqlConnection = New SqlConnection(mConnectionString)
                Using cmd As SqlCommand = conn.CreateCommand()
                    cmd.CommandType = Parameter.Type
                    cmd.CommandText = Parameter.Text
                    If Not Parameter.Items Is Nothing Then
                        For Each param As SqlParameter In Parameter.Items
                            cmd.Parameters.Add(param)
                        Next
                    End If
                    conn.Open()
                    entityList = BuildEntity(cmd)
                    If Not entityList Is Nothing Then
                        Return entityList
                    End If
                End Using
            End Using
        End If

        Return Nothing
    End Function
End Class

A continuación verá una clase de acceso a datos de muestra utilizando la base anterior para obtener una lista de productos.

Public Class ProductRepository
    Inherits Repository(Of Product)
    Implements IProductRepository

    Private mCache As IHttpCache

    'This const is what you will use in your app
    Public Sub New(ByVal cache As IHttpCache)
        MyBase.New()
        mCache = cache
    End Sub

    'This const is only used for testing so we can inject a connectin/transaction and have them roll'd back after the test
    Public Sub New(ByVal cache As IHttpCache, ByVal connection As IDbConnection, ByVal transaction As IDbTransaction)
        MyBase.New(connection, transaction)
        mCache = cache
    End Sub

    Public Function GetProducts() As System.Collections.Generic.List(Of Product) Implements IProductRepository.GetProducts
        Dim Parameter As New Parameter()
        Parameter.Type = CommandType.StoredProcedure
        Parameter.Text = "spGetProducts"
        Dim productList As List(Of Product)
        productList = MyBase.ExecuteReader(Parameter)
        Return productList
    End Function

    'This function is used in each class that inherits from the base data access class so we can keep all the boring left-right mapping code in 1 place per object
    Public Overrides Function BuildEntity(ByVal cmd As System.Data.SqlClient.SqlCommand) As System.Collections.Generic.List(Of Product)
        Dim productList As New List(Of Product)
        Using reader As SqlDataReader = cmd.ExecuteReader()
            Dim product As Product
            While reader.Read()
                product = New Product()
                product.ID = reader("ProductID")
                product.SupplierID = reader("SupplierID")
                product.CategoryID = reader("CategoryID")
                product.ProductName = reader("ProductName")
                product.QuantityPerUnit = reader("QuantityPerUnit")
                product.UnitPrice = reader("UnitPrice")
                product.UnitsInStock = reader("UnitsInStock")
                product.UnitsOnOrder = reader("UnitsOnOrder")
                product.ReorderLevel = reader("ReorderLevel")
                productList.Add(product)
            End While
            If productList.Count > 0 Then
                Return productList
            End If
        End Using
        Return Nothing
    End Function
End Class

Y ahora en su prueba unitaria también puede heredar de una clase base muy simple que realiza su trabajo de configuración/reversión, o mantener esto por prueba unitaria.

a continuación se muestra la clase base de prueba simple que utilicé

Imports System.Configuration
Imports System.Data
Imports System.Data.SqlClient
Imports Microsoft.VisualStudio.TestTools.UnitTesting

Public MustInherit Class TransactionFixture
    Protected mConnection As IDbConnection
    Protected mTransaction As IDbTransaction
    Private mConnectionString As String = ConfigurationManager.ConnectionStrings("Northwind.ConnectionString").ConnectionString

    <TestInitialize()> _
    Public Sub CreateConnectionAndBeginTran()
        mConnection = New SqlConnection(mConnectionString)
        mConnection.Open()
        mTransaction = mConnection.BeginTransaction()
    End Sub

    <TestCleanup()> _
    Public Sub RollbackTranAndCloseConnection()
        mTransaction.Rollback()
        mTransaction.Dispose()
        mConnection.Close()
        mConnection.Dispose()
    End Sub
End Class

y finalmente, la siguiente es una prueba simple que utiliza esa clase base de prueba que muestra cómo probar todo el ciclo CRUD para asegurarse de que todos los sprocs hagan su trabajo y que su código de ado.net realice el mapeo de izquierda a derecha correctamente.

Sé que esto no prueba el proceso "spGetProducts" utilizado en el ejemplo de acceso a datos anterior, pero deberías ver el poder detrás de este enfoque para los procesos de prueba unitaria.

Imports SampleApplication.Library
Imports System.Collections.Generic
Imports Microsoft.VisualStudio.TestTools.UnitTesting

<TestClass()> _
Public Class ProductRepositoryUnitTest
    Inherits TransactionFixture

    Private mRepository As ProductRepository

    <TestMethod()> _
    Public Sub Should-Insert-Update-And-Delete-Product()
        mRepository = New ProductRepository(New HttpCache(), mConnection, mTransaction)
        '** Create a test product to manipulate throughout **'
        Dim Product As New Product()
        Product.ProductName = "TestProduct"
        Product.SupplierID = 1
        Product.CategoryID = 2
        Product.QuantityPerUnit = "10 boxes of stuff"
        Product.UnitPrice = 14.95
        Product.UnitsInStock = 22
        Product.UnitsOnOrder = 19
        Product.ReorderLevel = 12
        '** Insert the new product object into SQL using your insert sproc **'
        mRepository.InsertProduct(Product)
        '** Select the product object that was just inserted and verify it does exist **'
        '** Using your GetProductById sproc **'
        Dim Product2 As Product = mRepository.GetProduct(Product.ID)
        Assert.AreEqual("TestProduct", Product2.ProductName)
        Assert.AreEqual(1, Product2.SupplierID)
        Assert.AreEqual(2, Product2.CategoryID)
        Assert.AreEqual("10 boxes of stuff", Product2.QuantityPerUnit)
        Assert.AreEqual(14.95, Product2.UnitPrice)
        Assert.AreEqual(22, Product2.UnitsInStock)
        Assert.AreEqual(19, Product2.UnitsOnOrder)
        Assert.AreEqual(12, Product2.ReorderLevel)
        '** Update the product object **'
        Product2.ProductName = "UpdatedTestProduct"
        Product2.SupplierID = 2
        Product2.CategoryID = 1
        Product2.QuantityPerUnit = "a box of stuff"
        Product2.UnitPrice = 16.95
        Product2.UnitsInStock = 10
        Product2.UnitsOnOrder = 20
        Product2.ReorderLevel = 8
        mRepository.UpdateProduct(Product2) '**using your update sproc
        '** Select the product object that was just updated to verify it completed **'
        Dim Product3 As Product = mRepository.GetProduct(Product2.ID)
        Assert.AreEqual("UpdatedTestProduct", Product2.ProductName)
        Assert.AreEqual(2, Product2.SupplierID)
        Assert.AreEqual(1, Product2.CategoryID)
        Assert.AreEqual("a box of stuff", Product2.QuantityPerUnit)
        Assert.AreEqual(16.95, Product2.UnitPrice)
        Assert.AreEqual(10, Product2.UnitsInStock)
        Assert.AreEqual(20, Product2.UnitsOnOrder)
        Assert.AreEqual(8, Product2.ReorderLevel)
        '** Delete the product and verify it does not exist **'
        mRepository.DeleteProduct(Product3.ID)
        '** The above will use your delete product by id sproc **'
        Dim Product4 As Product = mRepository.GetProduct(Product3.ID)
        Assert.AreEqual(Nothing, Product4)
    End Sub

End Class

Sé que este es un ejemplo largo, pero me ayudó tener una clase reutilizable para el trabajo de acceso a datos y otra clase reutilizable para mis pruebas, así no tuve que hacer el trabajo de configuración/desmontaje una y otra vez;)

Otros consejos

Has probado Unidad DB?Está diseñado para realizar pruebas unitarias de su base de datos, y solo de su base de datos, sin necesidad de revisar su código C#.

Si piensa en el tipo de código que las pruebas unitarias tienden a promover:pequeñas rutinas altamente cohesivas y poco acopladas, entonces debería poder ver dónde podría estar al menos parte del problema.

En mi mundo cínico, los procedimientos almacenados son parte del antiguo intento del mundo RDBMS de persuadirlo para que mueva su procesamiento comercial a la base de datos, lo cual tiene sentido si se considera que los costos de licencia del servidor tienden a estar relacionados con cosas como el número de procesadores.Cuantas más cosas ejecutes dentro de tu base de datos, más ganarán contigo.

Pero tengo la impresión de que en realidad estás más preocupado por el rendimiento, que en realidad no es exclusivo de las pruebas unitarias.Se supone que las pruebas unitarias son bastante atómicas y están destinadas a comprobar el comportamiento más que el rendimiento.Y en ese caso, es casi seguro que necesitarás cargas de clase de producción para poder verificar los planes de consulta.

Creo que necesitas una clase diferente de entorno de prueba.Sugeriría una copia de producción como la más simple, suponiendo que la seguridad no sea un problema.Luego, para cada versión candidata, comienza con la versión anterior, migra utilizando sus procedimientos de versión (que les darán una buena prueba como efecto secundario) y ejecuta sus tiempos.

Algo como eso.

La clave para probar procedimientos almacenados es escribir un script que complete una base de datos en blanco con datos planificados de antemano para dar como resultado un comportamiento consistente cuando se llamen a los procedimientos almacenados.

Tengo que votar a favor de favorecer en gran medida los procedimientos almacenados y colocar su lógica de negocios donde yo (y la mayoría de los administradores de bases de datos) creemos que pertenece, en la base de datos.

Sé que nosotros, como ingenieros de software, queremos que un código bellamente refactorizado, escrito en nuestro lenguaje favorito, contenga toda nuestra lógica importante, pero las realidades del rendimiento en sistemas de gran volumen y la naturaleza crítica de la integridad de los datos nos exigen hacer algunos compromisos. .El código SQL puede ser feo, repetitivo y difícil de probar, pero no puedo imaginar la dificultad de ajustar una base de datos sin tener un control total sobre el diseño de las consultas.

A menudo me veo obligado a rediseñar completamente las consultas, incluir cambios en el modelo de datos para que todo se ejecute en un período de tiempo aceptable.Con los procedimientos almacenados, puedo asegurar que los cambios serán transparentes para quien llama, ya que un procedimiento almacenado proporciona una encapsulación excelente.

Supongo que desea realizar pruebas unitarias en MSSQL.En cuanto a DBUnit, existen algunas limitaciones en su compatibilidad con MSSQL.No es compatible con NVarChar, por ejemplo. Aquí hay algunos usuarios reales y sus problemas con DBUnit.

Buena pregunta.

Tengo problemas similares y he tomado el camino de menor resistencia (para mí, al menos).

Hay muchas otras soluciones, que otros han mencionado.Muchos de ellos son mejores/más puros/más apropiados para otros.

Ya estaba usando Testdriven.NET/MbUnit para probar mi C#, así que simplemente agregué pruebas a cada proyecto para llamar a los procedimientos almacenados utilizados por esa aplicación.

Sé que sé.Esto suena terrible, pero lo que necesito es despegar con alguno pruebas y continuar desde allí.Este enfoque significa que, aunque mi cobertura es baja, estoy probando algunos procesos almacenados al mismo tiempo que pruebo el código que los llamará.Hay algo de lógica en esto.

Estoy exactamente en la misma situación que el cartel original.Todo se reduce a rendimiento versus capacidad de prueba.Mi preferencia es hacia la capacidad de prueba (hacer que funcione, hacerlo bien, hacerlo rápido), lo que sugiere mantener la lógica empresarial fuera de la base de datos.Las bases de datos no sólo carecen de los marcos de prueba, las construcciones de factorización de código y las herramientas de análisis y navegación de código que se encuentran en lenguajes como Java, sino que el código de base de datos altamente factorizado también es lento (donde el código Java altamente factorizado no lo es).

Sin embargo, reconozco el poder del procesamiento de conjuntos de bases de datos.Cuando se usa apropiadamente, SQL puede hacer cosas increíblemente poderosas con muy poco código.Por lo tanto, estoy de acuerdo con que haya cierta lógica basada en conjuntos en la base de datos, aunque todavía haré todo lo que pueda para probarla unitariamente.

En una nota relacionada, parece que un código de base de datos muy largo y procedimental es a menudo un síntoma de algo más, y creo que dicho código se puede convertir en código comprobable sin incurrir en un impacto en el rendimiento.La teoría es que dicho código a menudo representa procesos por lotes que procesan periódicamente grandes cantidades de datos.Si estos procesos por lotes se convirtieran en fragmentos más pequeños de lógica de negocios en tiempo real que se ejecuta cada vez que se cambian los datos de entrada, esta lógica podría ejecutarse en el nivel medio (donde se puede probar) sin afectar el rendimiento (ya que el trabajo se realiza en pequeñas porciones en tiempo real).Como efecto secundario, esto también elimina los largos ciclos de retroalimentación del manejo de errores del proceso por lotes.Por supuesto, este enfoque no funcionará en todos los casos, pero puede funcionar en algunos.Además, si hay toneladas de código de base de datos de procesamiento por lotes no comprobable en su sistema, el camino hacia la salvación puede ser largo y arduo.YMMV.

Pero tengo la impresión de que en realidad estás más preocupado por el rendimiento, que en realidad no es exclusivo de las pruebas unitarias.Se supone que las pruebas unitarias son bastante atómicas y están destinadas a comprobar el comportamiento más que el rendimiento.Y en ese caso, es casi seguro que necesitarás cargas de clase de producción para poder verificar los planes de consulta.

Creo que aquí hay dos áreas de prueba bastante distintas:el rendimiento y la lógica real de los procedimientos almacenados.

Di el ejemplo de probar el rendimiento de la base de datos en el pasado y, afortunadamente, hemos llegado a un punto en el que el rendimiento es suficientemente bueno.

Estoy completamente de acuerdo en que la situación con toda la lógica empresarial en la base de datos es mala, pero es algo que hemos heredado antes de que la mayoría de nuestros desarrolladores se unieran a la empresa.

Sin embargo, ahora estamos adoptando el modelo de servicios web para nuestras nuevas características y hemos estado tratando de evitar los procedimientos almacenados tanto como sea posible, manteniendo la lógica en el código C# y ejecutando comandos SQL en la base de datos (aunque ahora linq sería el método preferido).Todavía hay cierto uso de los SP existentes, por lo que estaba pensando en realizar pruebas unitarias de forma retrospectiva.

También puedes probar Visual Studio para profesionales de bases de datos.Se trata principalmente de gestión de cambios, pero también tiene herramientas para generar datos de prueba y pruebas unitarias.

Aunque es bastante caro.

Usamos Datos frescos para deshacer los cambios entre cada prueba, entonces probar sprocs es relativamente fácil.

Lo que todavía falta son herramientas de cobertura de código.

Hago pruebas unitarias para pobres.Si soy vago, la prueba es solo un par de invocaciones válidas con valores de parámetros potencialmente problemáticos.

/*

--setup
Declare @foo int Set @foo = (Select top 1 foo from mytable)

--test
execute wish_I_had_more_Tests @foo

--look at rowcounts/look for errors
If @@rowcount=1 Print 'Ok!' Else Print 'Nokay!'

--Teardown
Delete from mytable where foo = @foo
*/
create procedure wish_I_had_more_Tests
as
select....

LINQ simplificará esto sólo si elimina la lógica de sus procedimientos almacenados y la vuelve a implementar como consultas linq.Lo cual sería mucho más robusto y más fácil de probar, definitivamente.Sin embargo, parece que sus requisitos lo impedirían.

TL;DR:Tu diseño tiene problemas.

Realizamos pruebas unitarias del código C# que llama a los SP.
Contamos con scripts de compilación, creando bases de datos de prueba limpias.
Y los más grandes los colocamos y separamos durante la prueba.
Estas pruebas pueden llevar horas, pero creo que vale la pena.

Una opción para refactorizar el código (admito que es un truco feo) sería generarlo mediante CPP (el preprocesador de C) M4 (nunca lo probé) o similar.Tengo un proyecto que está haciendo precisamente eso y en realidad es prácticamente viable.

El único caso para el que creo que podría ser válido es 1) como una alternativa a los procedimientos almacenados KLOC+ y 2) y estos son mis casos, cuando el objetivo del proyecto es ver hasta qué punto (hasta la locura) se puede impulsar una tecnología.

Oh chico.Los sprocs no se prestan a pruebas unitarias (automatizadas).Hago una especie de "prueba unitaria" de mis sprocs complejos escribiendo pruebas en archivos por lotes t-sql y comprobando manualmente la salida de las declaraciones impresas y los resultados.

El problema con las pruebas unitarias de cualquier tipo de programación relacionada con datos es que, para empezar, es necesario tener un conjunto confiable de datos de prueba.Mucho también depende de la complejidad del proceso almacenado y de lo que hace.Sería muy difícil automatizar las pruebas unitarias para un procedimiento muy complejo que modifica muchas tablas.

Algunos de los otros carteles han señalado algunas formas sencillas de automatizar las pruebas manuales y también algunas herramientas que puede utilizar con SQL Server.Del lado de Oracle, el gurú de PL/SQL Steven Feuerstein trabajó en una herramienta de prueba unitaria gratuita para procedimientos almacenados de PL/SQL llamada utPLSQL.

Sin embargo, abandonó ese esfuerzo y luego se puso a comercializar el Code Tester de Quest para PL/SQL.Quest ofrece una versión de prueba descargable gratuita.Estoy a punto de probarlo;Según tengo entendido, es bueno para ocuparse de los gastos generales al configurar un marco de prueba para que pueda concentrarse solo en las pruebas en sí, y mantiene las pruebas para que pueda reutilizarlas en las pruebas de regresión, uno de los grandes beneficios de desarrollo basado en pruebas.Además, se supone que es bueno para algo más que verificar una variable de salida y tiene disposiciones para validar cambios de datos, pero todavía tengo que examinarlo más de cerca.Pensé que esta información podría ser valiosa para los usuarios de Oracle.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top