37 votes

Quelqu'un a-t-il réussi à tester des procédures stockées SQL?

Nous avons constaté que les tests unitaires que nous avons écrit pour notre C#/C++ code ont vraiment payé. Mais nous avons encore des milliers de lignes de la logique métier dans les procédures stockées, qui ne deviennent vraiment testé en colère quand notre produit est étendu à un grand nombre d'utilisateurs.

Ce qui rend ce pire, c'est que certaines de ces procédures stockées finir par être très long, car le gain de performance lors du passage de tables temporaires entre SPs. Cela nous a empêchés de nous refactoring pour rendre le code plus simple.

Nous avons fait plusieurs tentatives pour construire des tests d'unité autour de certaines de nos principales procédures stockées (principalement à tester les performances), mais j'ai trouvé que la mise en place des données de test pour ces tests est vraiment dur. Par exemple, nous sommes en fin de copie autour de bases de données d'essai. En plus de cela, les tests finissent par être vraiment sensible au changement, et même le moindre changement à une procédure stockée. ou une table nécessite une grande quantité de changements pour les tests. Ainsi, après de nombreuses construit brisent à cause de ces tests de base de données à défaut par intermittence, nous venons juste de les sortir du processus de construction.

Ainsi, la majeure partie de mes questions est: quelqu'un a réussi à inscrire des tests unitaires pour leurs procédures stockées?

La deuxième partie de mes questions est de savoir si les tests unitaires serait/est plus facile avec linq?

Je pensais que, plutôt que d'avoir à mettre en place des tableaux de données de test, vous pouvez créer simplement une collection d'objets de test et tester votre code linq dans un "linq to objects" situation? (Je suis totalement nouveau pour linq donc ne sais pas si ce serait même travail à tous)

12voto

Toran Billups Points 10012

Je suis tombé sur cette même question, un temps, et a constaté que si je créé une simple classe de base abstraite pour l'accès aux données qui m'ont permis d'injecter une connexion et une opération, j'ai pu test unitaire mon sprocs pour voir si ils ont fait le travail dans SQL que je leur ai demandé de faire et de restauration, ainsi toutes les données de test est à gauche dans la db.

Il se sentait mieux que l'habituel "exécuter un script pour l'installation de mon essai db, puis après l'exécution des tests faire un nettoyage de la junk/données de test". Cela a également senti plus proche de l'unité de test parce que ces tests pourraient être courir seul, sans avoir beaucoup de "tout dans la db doit être" seulement si "avant de me lancer ces tests".

Voici un extrait de la classe de base abstraite utilisé pour accéder aux données

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

ensuite, vous verrez un échantillon d'accès aux données de la classe à l'aide de la base pour obtenir une liste de produits

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

Et maintenant, dans votre unité de test, vous pouvez également hériter d'une très simple de la classe de base qu'est-ce que votre programme d'installation / restauration travail ou de garder ce par unité de test de base

ci-dessous est la simple mise à l'essai de la classe de base que j'ai utilisé

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

et enfin - le ci-dessous est un simple test à l'aide de test de la classe de base qui montre comment tester l'ensemble du CRUD cycle pour s'assurer que tous les sprocs faire leur travail et que votre ado.net code de la gauche-droite de la cartographie correctement

Je sais que ce n'est pas le test du "spGetProducts" la procédure stockée est utilisé à l'accès aux données de l'échantillon, mais vous devriez voir la puissance derrière cette approche, les tests unitaires, sprocs

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

Je sais que c'est un exemple long, mais il a aidé à avoir une classe réutilisable pour l'accès aux données de travail, et encore une autre classe réutilisable pour mes tests, donc je n'ai pas à faire l'installation/démontage travailler encore et encore ;)

10voto

Jon Limjap Points 46429

Avez-vous essayé DBUnit ? Il est conçu pour tester votre base de données, et uniquement votre base de données, sans passer par votre code C #.

6voto

Mike Woodhouse Points 27748

Si vous pensez à ce sujet le genre de code que le test unitaire tend à promouvoir: petit hautement cohésif et humble couplé à des routines, alors vous devriez à peu près en mesure de voir où au moins une partie du problème pourrait être.

Dans mon monde cynique, procédures stockées sont une partie du SGBDR monde de longue date de tenter de vous convaincre de faire évoluer votre entreprise de traitement dans la base de données, ce qui est logique si l'on considère que serveur de licences, les coûts ont tendance à être liées à des choses comme le nombre de processeurs. Les plus de choses que vous exécutez l'intérieur de votre base de données, plus ils le font de vous.

Mais j'ai l'impression que vous êtes en fait plus concerné par les performances, ce qui n'est pas vraiment l'apanage de l'unité de dépistage à tous. Les tests unitaires sont censés être assez atomique et sont destinées à vérifier le comportement plutôt que sur les performances. Et dans ce cas, vous êtes presque certainement allez avoir besoin de production-charges de classe afin de vérifier les plans de requête.

Je pense que vous avez besoin d'une autre classe d'environnement de test. Je vous suggère une copie de la production comme la plus simple, en supposant que la sécurité n'est pas un problème. Ensuite, pour chaque candidat, de presse, de commencer avec la version précédente, migrer à l'aide de votre communiqué de procédures (qui vont donner à ces un bon test comme un effet secondaire) et exécuter vos horaires.

Quelque chose comme ça.

6voto

Eric Z Beard Points 18473

La clé pour les tests de procédures stockées est d'écrire un script qui remplit un vide de la base de données avec des données qui est prévu à l'avance pour entraîner un comportement cohérent lorsque les procédures stockées sont appelés.

Je dois mettre mon vote pour fortement favorisant les procédures stockées et de la passation de votre logique métier où je (et la plupart des Administrateurs de base de données) pense qu'il appartient, dans la base de données.

Je sais que nous sommes comme les ingénieurs en logiciel voulez admirablement remaniée du code, écrit en notre langue préférée, pour contenir l'ensemble de notre important de la logique, mais les réalités de la performance dans un volume élevé de systèmes, et de la nature critique de l'intégrité des données, nous obligent à faire quelques compromis. Le code Sql peut être moche, répétitif, et il est difficile de tester, mais je ne peux pas imaginer la difficulté de paramétrage d'une base de données sans avoir un contrôle complet sur la conception des requêtes.

Je suis souvent contraint de revoir totalement la conception de requêtes, d'inclure les changements dans le modèle de données, pour obtenir des choses pour les exécuter dans une quantité de temps acceptable. Avec des procédures stockées, je peux vous assurer que le changement sera transparent pour l'appelant, depuis une procédure stockée offre excellent encapsulation.

4voto

RedWolves Points 5703

Je suppose que vous souhaitez effectuer des tests unitaires dans MSSQL. La prise en charge de MSSQL par DBUnit est limitée. Il ne supporte pas NVarChar par exemple. Voici quelques utilisateurs réels et leurs problèmes avec DBUnit.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X