2008-08-15 8 views
37

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 un gran número de usuarios.¿Alguien ha tenido éxito en la prueba unitaria de los procedimientos almacenados de SQL?

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 de crear pruebas unitarias sobre algunos de nuestros procedimientos almacenados clave (principalmente pruebas del rendimiento), pero hemos descubierto que configurar 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 linq en una situación "linq a los objetos". (Soy totalmente nuevo en linq, así que no sé si esto funcionaría en absoluto)

Respuesta

11

Me encontré con este mismo problema hace un tiempo y descubrí que si creaba una clase base abstracta simple para acceso a datos que me permitiera inyectar una conexión y transacción, podía probar mis sprocs para ver si funcionaban en SQL que les pedí que hicieran y luego lo retrotraigan para que ninguno de los datos de la prueba quede en el archivo db.

Me sentí mejor que el habitual "ejecutar un script para configurar mi db de prueba, luego, después de ejecutar las pruebas, hacer una limpieza de los datos de 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 se utiliza 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 

siguiente verá una clase de acceso a datos de la muestra usando la base de arriba 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 base por unidad de prueba

a continuación es la clase base de pruebas simples Solía ​​

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 - el siguiente es una prueba sencilla usando esa clase base de prueba que muestra cómo poner a prueba toda la ABM ciclo para asegurarse de que todos los procedimientos almacenados hacer su trabajo y que su código ado.net el mapeo de izquierda-derecha correctamente

sé que esto no prueba la sproc "spGetProducts" que se utiliza en la cA de datos por encima de muestra de Cess, pero hay que ver el poder detrás de este enfoque a sprocs 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 esto es un largo ejemplo, pero ayudó a tener una clase reutilizable para la obra de acceso a datos, y otra reutilizable clase para mis pruebas, así que no tuve que hacer el trabajo de configuración/desmontaje una y otra vez;)

10

¿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#.

0

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.

6

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

En mi mundo cínico, procedimientos almacenados son parte del intento de larga data del mundo RDBMS de persuadir a mover su proceso de negocio en la base de datos, lo cual tiene sentido si tenemos en cuenta que los costes de licencia del servidor tienden a estar relacionados con cosas como recuento de procesador. 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 necesita 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 así.

0

Probamos unitariamente el código C# que llama a los SP.
Tenemos scripts de construcción, creando bases de datos de pruebas limpias.
Y los más grandes los conectamos y separamos durante el montaje de la prueba.
Estas pruebas pueden llevar horas, pero creo que lo vale.

1

Usamos DataFresh para revertir los cambios entre cada prueba, luego probar los sprocs es relativamente fácil.

Lo que aún falta son herramientas de cobertura de código.

2

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.

He dado 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 completamente 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 compañía.

Sin embargo, ahora estamos adoptando el modelo de servicios web para nuestras nuevas características, y hemos tratado 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 linq ahora sería el método preferido). Todavía hay un cierto uso de los SP existentes, por lo que estaba pensando en probarlos retrospectivamente.

0

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 que creo que podría ser válido es 1) como alternativa a KLOC + procedimientos almacenados y 2) y este es mi caso, cuando el objetivo del proyecto es ver qué tan lejos (enloquecido) puede empujar una tecnología.

6

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 dar mi voto por favorecer fuertemente los procedimientos almacenados y ubicar la lógica de su negocio donde yo (y la mayoría de los DBA) cree que pertenece, en la base de datos.

Sé que como ingenieros de software queremos un código refactorizado hermoso, 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 para 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 por completo las consultas, para incluir cambios en el modelo de datos, para que las cosas se ejecuten 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.

+0

Usted estaba curioseando con su snswer, ¿o no? ¿Se usa la lógica de negocios en un sentido sobrecargado? Entonces, ¿cómo probarías tu lógica en una unidad, con integración o pruebas de stack más altas? He tenido mucho más éxito al mantener las compras simples y hacer un levantamiento lógico en el código. He tenido experiencias horribles con sprocs y funciones anidadas ridículamente para asegurarme de que toda la lógica comercial permanezca en DB. No para mí, gracias. – brumScouse

0

Oh, muchacho. 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.

0

El problema con la unidad de prueba de cualquier tipo de programación relacionada con datos es que para empezar tiene que 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.

2

También puedes 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.

3

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, reconozco el poder del procesamiento del conjunto 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 puede convertirse 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.

4

Buena pregunta.

Tengo problemas similares, y he tomado el camino de 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#, así que simplemente agregué pruebas a cada proyecto para llamar a los procedimientos almacenados utilizados por esa aplicación.

Lo sé, lo sé. Esto suena terrible, pero lo que necesito es despegar con algunas pruebas de, 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.

1

Realizo pruebas de unidades para personas 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.... 
Cuestiones relacionadas