¿Alguien ha tenido éxito en la prueba unitaria de los procedimientos almacenados de SQL?
unit-testing linq-to-sql (16)
Hemos descubierto que las pruebas unitarias que hemos escrito para nuestro código C # / C ++ realmente han valido la pena. Pero todavía tenemos miles de líneas de lógica de negocios en procedimientos almacenados, que solo se prueban con ira 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 rendimiento alcanzado al pasar tablas temporales entre SP. Esto nos ha impedido refactorizar para simplificar el código.
Hemos realizado varios intentos para crear pruebas unitarias en torno a algunos de nuestros procedimientos almacenados clave (principalmente pruebas del rendimiento), pero hemos descubierto que la configuración de los datos de prueba para estas pruebas es realmente difícil. Por ejemplo, terminamos copiando alrededor de las bases de datos de prueba. Además de esto, las pruebas terminan siendo realmente sensibles al cambio, e incluso el más pequeño cambio a un proceso almacenado. o la tabla requiere una gran cantidad de cambios en las pruebas. Entonces, después de muchas compilaciones que se rompen debido a que estas pruebas de bases de datos fallan intermitentemente, simplemente tuvimos que sacarlas del proceso de compilación.
Entonces, la parte principal de mis preguntas es: ¿alguna vez alguien ha escrito con éxito pruebas unitarias para sus procedimientos almacenados?
La segunda parte de mis preguntas es si la prueba unitaria sería / es más fácil con linq?
Estaba pensando que, en lugar de tener que configurar tablas de datos de prueba, simplemente podría crear una colección de objetos de prueba y probar su código de linq en una situación de "linq a los objetos". (Soy totalmente nuevo en linq, así que no sé si esto funcionaría en absoluto)
Si piensas en el tipo de código que las pruebas unitarias tienden a promover: pequeñas rutinas muy cohesionadas y poco acopladas, entonces deberías 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 RDBMS para persuadirte de que muevas el procesamiento de tu negocio a la base de datos, lo que tiene sentido si consideras que los costos de la licencia de servidor suelen estar relacionados con cosas como el recuento de procesadores. Cuantas más cosas ejecutes dentro de tu base de datos, más harán de ti.
Pero me da la impresión de que en realidad está 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 verificar el comportamiento en lugar del rendimiento. Y en ese caso, seguramente necesitará cargas de la clase de producción para 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 es un problema. Luego, para cada versión candidata, empiezas con la versión anterior, migras usando tus procedimientos de lanzamiento (lo que les dará una buena prueba como efecto secundario) y ejecutas tus tiempos.
Algo como eso.
Pero me da la impresión de que en realidad está 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 verificar el comportamiento en lugar del rendimiento. Y en ese caso, seguramente necesitará cargas de la clase de producción para 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 DB en el pasado y, afortunadamente, hemos llegado a un punto donde el rendimiento es lo suficientemente bueno.
Estoy totalmente de acuerdo en que la situación con toda la lógica de negocios 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 funciones, y hemos estado tratando de evitar los procedimientos almacenados tanto como sea posible, manteniendo la lógica en el código C # y disparando SQLCommands en la base de datos (aunque ahora linq el método preferido). Todavía hay un cierto uso de los SP existentes, por lo que estaba pensando en probarlos retrospectivamente.
¿Has probado DBUnit ? Está diseñado para probar su base de datos, y solo su base de datos, sin necesidad de revisar su código C #.
LINQ simplificará esto solo 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 fácil de probar, definitivamente. Sin embargo, parece que sus requisitos lo impedirían.
TL; DR: su diseño tiene problemas.
La clave para probar los procedimientos almacenados es escribir una secuencia de comandos que llene una base de datos en blanco con datos planificados con anticipación para generar un comportamiento consistente cuando se invocan los procedimientos almacenados.
Tengo que poner mi voto a favor de los procedimientos almacenados y ubicar su lógica de negocios donde yo (y la mayoría de los DBA) creo que pertenece, en la base de datos.
Sé que nosotros, como ingenieros de software, queremos un código bellamente refactorizado, escrito en nuestro idioma favorito, para contener toda nuestra lógica importante, pero las realidades del rendimiento en sistemas de gran volumen y la naturaleza crítica de la integridad de datos requieren que hagamos 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 por completo las consultas, para incluir cambios en el modelo de datos, para que las cosas funcionen en un tiempo aceptable. Con los procedimientos almacenados, puedo asegurar que los cambios serán transparentes para la persona que llama, ya que un procedimiento almacenado proporciona una encapsulación tan excelente.
Nosotros probamos unitariamente el código C # que llama a los SP.
Tenemos scripts de compilación, creando bases de datos de pruebas limpias.
Y los más grandes que conectamos y separamos durante el accesorio de la prueba.
Estas pruebas pueden llevar horas, pero creo que vale la pena.
Oh chico. los sprocs no se prestan a pruebas unitarias (automáticas). Clasifico mis "pruebas unitarias" de mis sprocs complejos escribiendo pruebas en archivos batch t-sql y comprobando manualmente el resultado de las declaraciones de impresión y los resultados.
Supongo que quieres pruebas unitarias en MSSQL. En cuanto a DBUnit, hay algunas limitaciones en su soporte para MSSQL. No es compatible con NVarChar, por ejemplo. Aquí hay algunos usuarios reales y sus problemas con DBUnit.
Una opción para volver a factorizar el código (admitiré un hack feo) sería generarlo a través de CPP (el preprocesador C) M4 (nunca lo intenté) o similar. Tengo un proyecto que está haciendo exactamente eso y que en realidad es más viable.
El único caso en el que creo que podría ser válido es 1) como alternativa a los procedimientos almacenados KLOC + y 2) y este es mi caso, cuando el objetivo del proyecto es ver qué tan lejos (en locura) puede impulsar una tecnología.
Usamos DataFresh para deshacer los cambios entre cada prueba, luego probar sprocs es relativamente fácil.
Lo que aún falta es herramientas de cobertura de código.
El problema con la prueba unitaria de cualquier tipo de programación relacionada con los datos es que para empezar, debe 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 modificó muchas tablas.
Algunos de los otros carteles han notado algunas formas simples de automatizar las pruebas manuales, y también algunas herramientas que puede usar con SQL Server. En el lado de Oracle, el gurú PL / SQL Steven Feuerstein trabajó en una herramienta de prueba gratuita para procedimientos almacenados PL / SQL llamada utPLSQL.
Sin embargo, abandonó ese esfuerzo y luego se comercializó con Quest''s Code Tester para PL / SQL. Quest ofrece una versión de prueba descargable gratuita. Estoy a punto de probarlo; Tengo entendido que es bueno ocuparse de los gastos generales al establecer un marco de prueba para que pueda centrarse solo en las pruebas, y mantiene las pruebas para que pueda volver a utilizarlas en las pruebas de regresión, uno de los grandes beneficios de test-driven-development. Además, se supone que es bueno para algo más que simplemente verificar una variable de salida y tiene una provisión para validar los cambios en los datos, pero aún tengo que echarle un vistazo más de cerca. Pensé que esta información podría ser valiosa para los usuarios de Oracle.
También puede probar Visual Studio for Database Professionals . Se trata principalmente de la gestión del cambio, pero también cuenta con herramientas para generar datos de prueba y pruebas unitarias.
Es bastante caro aunque
Me encontré con este mismo problema hace un tiempo y descubrí que si creaba una clase base abstracta simple para el acceso a los datos que me permitía inyectar una conexión y transacción, podía probar mis sprocs para ver si hacían el trabajo en SQL que yo les pidió que hicieran y luego retrocedieran para que ninguno de los datos de la prueba quedara en el db.
Me sentí mejor que el habitual "ejecutar un script para configurar mi prueba db, luego, después de ejecutar las pruebas, hacer una limpieza de los datos basura / prueba". Esto también se sintió más cerca de las pruebas unitarias porque estas pruebas podrían ejecutarse solas sin tener una gran cantidad de "todo en el DB debe ser ''así'' antes de ejecutar estas pruebas".
Aquí hay un fragmento de la clase base abstracta utilizada para el acceso a los 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 de unidad, también puede heredar de una clase base muy simple que hace su trabajo de configuración / retrotracción, o mantener esto en una prueba por unidad
a continuación es 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 usando esa clase base de prueba que muestra cómo probar todo el ciclo CRUD para asegurarse de que todos los sprocs hacen su trabajo y que su código ado.net hace el mapeo izquierda-derecha correctamente
Sé que esto no prueba el sproc "spGetProducts" utilizado en la muestra de acceso a datos anterior, pero debería ver el poder detrás de este enfoque para probar pruebas unitarias
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 largo ejemplo, pero ayudó a tener una clase reutilizable para el trabajo de acceso a datos, y otra clase reutilizable para mis pruebas, así que no tuve que hacer el trabajo de configuración / desmontaje una y otra vez;)
Buena pregunta.
Tengo problemas similares, y he tomado el camino de la menor resistencia (para mí, de todos modos).
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 #, por lo 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 algunas pruebas e ir desde allí. Este enfoque significa que, aunque mi cobertura es baja, estoy probando algunos procesos almacenados al mismo tiempo que estoy probando el código que los llamará. Hay algo de lógica en esto.
Estoy en la misma situación que el póster original. Todo se reduce al rendimiento frente a la capacidad de prueba. Mi prejuicio es hacia la capacidad de prueba (hacer que funcione, hacerlo bien, hacerlo rápido), lo que sugiere mantener la lógica comercial fuera de la base de datos. Las bases de datos no solo carecen de marcos de prueba, construcciones de factorización de código y herramientas de navegación y análisis de código que se encuentran en lenguajes como Java, pero 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, sí reconozco el poder del procesamiento de conjuntos de bases de datos. Cuando se usa apropiadamente, SQL puede hacer algunas cosas increíblemente poderosas con muy poco código. Por lo tanto, estoy de acuerdo con una lógica basada en conjunto que vive en la base de datos, aunque seguiré haciendo todo lo que pueda para probarla en una unidad.
En una nota relacionada, parece que el código de base de datos muy largo y de procedimiento es a menudo un síntoma de otra cosa, y creo que dicho código se puede convertir en código comprobable sin incurrir en un golpe de 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 trozos más pequeños de lógica comercial 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 tomar un golpe de rendimiento (ya que el trabajo se realiza en pequeños fragmentos en tiempo real). Como efecto colateral, 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 dicho código de base de datos de procesamiento discontinuo no comprobable en su sistema, el camino hacia la salvación puede ser largo y arduo. YMMV.
Hago las pruebas unitarias de pobres. Si soy perezoso, 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....