Quelqu'un a-t-il réussi à tester les procédures stockées de SQL à l'unité?

nous avons trouvé que les tests unitaires que nous avons écrits pour notre code C#/C++ ont vraiment payé. Mais nous avons encore des milliers de lignes de logique commerciale dans les procédures stockées, qui ne sont vraiment testés dans la colère lorsque notre produit est déployé à un grand nombre d'utilisateurs.

ce qui rend cette situation encore pire est que certaines de ces procédures stockées finissent par être très longues, en raison de la performance hit lors du passage des tables temporaires entre les SPs. Cela nous a empêchés de nous remaniement pour simplifier le code.

nous avons essayé à plusieurs reprises de construire des tests unitaires autour de certaines de nos procédures clés stockées (principalement en testant la performance), mais nous avons constaté que la configuration des données de test pour ces tests est vraiment difficile. Par exemple, nous finissons par copier autour des bases de données de test. 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 tableau nécessite un grand nombre de modifications à la test. Donc, après de nombreux builds cassés en raison de ces tests de base de données qui échouent de façon intermittente, nous avons juste eu à les retirer du processus de construction.

donc, la partie principale de mes questions Est: est-ce que quelqu'un a déjà réussi à écrire des tests unitaires pour leurs procédures stockées?

la deuxième partie de mes questions est de savoir si les tests unitaires seraient/sont plus faciles avec linq?

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

37
demandé sur Chris 2008-08-15 19:29:14

16 réponses

j'ai rencontré ce même problème il y a quelque temps et j'ai découvert que si je créais une classe de base abstraite simple pour l'accès aux données qui me permettait d'injecter une connexion et une transaction, je pourrais unit tester mes sproc pour voir s'ils ont fait le travail en SQL que je leur ai demandé de faire et ensuite faire un rollback afin qu'aucune des données de test ne soit laissée dans le db.

c'était mieux que d'habitude "exécuter un script pour configurer mon test db, puis après les tests exécuter faire un nettoyage de la camelote/données de test". Ce on se sent aussi plus proche des tests unitaires parce que ces tests peuvent être exécutés seuls, avec beaucoup de "tout ce qui est dans la base de données doit être"juste ainsi" avant que je ne fasse ces tests".

voici un extrait de la classe de base abstraite utilisée pour l'accès 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

suivant vous verrez une classe d'accès aux données d'échantillon en utilisant la base ci-dessus 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 test unitaire, vous pouvez aussi hériter d'une classe de base très simple qui fait votre travail de configuration / rollback - ou garder cela sur une base de test unitaire

ci-dessous est la base d'essai simple classe I 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 - ci-dessous est un test simple en utilisant cette classe de base de test qui montre comment tester le cycle complet de CRUD pour s'assurer que tous les sproc faire leur travail et que votre ado.net code de la gauche-droite de la cartographie correctement

je sais que cela ne teste pas le sproc" spGetProducts "utilisé dans l'échantillon d'accès aux données ci-dessus, mais vous devriez voir la puissance derrière cette approche aux sproc

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 long exemple, mais il a aidé à avoir une classe réutilisable pour le travail d'accès aux données, et encore une autre classe réutilisable pour mes tests donc je n'ai pas eu à faire le réglage/démontage travailler encore et encore ;)

12
répondu Toran Billups 2008-08-26 12:12:58

avez-vous essayé DBUnit ? Il est conçu pour tester votre base de données, et juste votre base de données, sans avoir besoin de passer en revue votre code C#.

10
répondu Jon Limjap 2008-08-15 15:35:02

si vous pensez au genre de code que les tests unitaires tendent à promouvoir: petites routines très cohésives et faiblement couplées, alors vous devriez être assez en mesure de voir où au moins une partie du problème pourrait être.

dans mon monde cynique, les procédures stockées font partie de la tentative de longue date de RDBMS monde pour vous persuader de déplacer votre traitement des affaires dans la base de données, ce qui a du sens lorsque vous considérez que les coûts de licence de serveur ont tendance à être liés à des choses comme le nombre de processeurs. Plus de choses que vous exécutez dans votre base de données, plus ils font de vous.

mais j'ai l'impression que vous êtes en fait plus concerné par la performance, qui n'est pas vraiment l'apanage des tests unitaires. Les essais unitaires sont censés être assez atomiques et visent à vérifier le comportement plutôt que la performance. Et dans ce cas, vous allez presque certainement avoir besoin de charges de classe de production afin de vérifier les plans de requête.

je pense que vous avez besoin d'une autre classe d'environnement de test. Je suggère une copie de la production comme la plus simple, en supposant que la sécurité ne soit pas un problème. Ensuite, pour chaque version candidate, vous commencez avec la version précédente, migrez en utilisant vos procédures de publication (ce qui donnera un bon test comme effet secondaire) et exécutez vos timings.

quelque chose comme ça.

6
répondu Mike Woodhouse 2008-08-15 15:51:15

la clé pour tester les procédures stockées est d'écrire un script qui remplit une base de données vierge avec des données qui sont planifiées à l'avance pour avoir un comportement cohérent lorsque les procédures stockées sont appelées.

je dois mettre mon vote pour fortement favoriser les procédures stockées et placer votre logique d'affaires où je (et la plupart des AD) pense qu'il appartient, dans la base de données.

je sais que nous, en tant qu'ingénieurs logiciels, voulons un code magnifiquement remanié, écrit dans notre langue préférée, pour contenir toute notre logique importante, mais les réalités de la performance dans les systèmes à haut volume, et la nature critique de l'intégrité des données, nous obligent à faire des compromis. Code Sql peut être laid, répétitif, et difficile à tester, mais je ne peux pas imaginer la difficulté d'accorder une base de données sans avoir le contrôle complet sur la conception des requêtes.

je suis souvent forcé de reconcevoir complètement les requêtes, pour inclure des changements au modèle de données, pour faire tourner les choses dans un délai acceptable. Avec des procédures stockées, je peux assurer que les changements seront transparents pour l'appelant, puisqu'une procédure stockée fournit une telle excellente encapsulation.

6
répondu Eric Z Beard 2008-08-15 17:27:51

je suppose que vous voulez des tests unitaires en MSSQL. En regardant DBUnit il y a quelques limites dans le soutien de it pour MSSQL. Il ne supporte pas NVarChar par exemple. voici quelques utilisateurs réels et leurs problèmes avec DBUnit.

4
répondu RedWolves 2008-08-15 15:40:50

bonne question.

j'ai des problèmes similaires, et j'ai pris la voie de la moindre résistance (pour moi, en tout cas).

il y a un tas d'autres solutions, que d'autres ont mentionnées. Beaucoup d'entre eux sont meilleurs / plus purs / plus appropriés pour les autres.

j'utilisais déjà Testdriven.NET/MbUnit pour tester mon c#, donc j'ai simplement ajouté des tests à chaque projet pour appeler les procédures stockées utilisées par cette application.

je sais, je sais. Cela semble terrible, mais ce dont j'ai besoin est de démarrer avec quelque Test, et aller de là. Cette approche signifie que même si ma couverture est faible, je teste certains procs stockés en même temps que je teste le code qui les appellera. Il y a une certaine logique à cela.

4
répondu AJ. 2008-10-02 11:40:52

je suis exactement dans la même situation que l'affiche originale. Il s'agit de la performance par rapport à la testabilité. Mon parti pris est vers la testabilité (le faire fonctionner, faire droit, rendre rapide), qui suggère de garder la logique métier de la base de données. Les bases de données ne sont pas seulement dépourvues des cadres de test, des constructions de factoring de code, et des outils d'analyse de code et de navigation trouvés dans des langues comme Java, mais le code de base de données fortement factorisé est également lent (où le code Java fortement factorisé n'est pas).

cependant, je reconnais la puissance du traitement des ensembles de base de données. LORSQU'il est utilisé correctement, SQL peut faire des choses incroyablement puissantes avec très peu de code. Donc, je suis d'accord avec une certaine logique basée sur les ensembles vivant dans la base de données même si je vais encore faire tout ce que je peux pour l'Unité de test.

sur une note apparentée, il semble que le code de base de données très long et procédural est souvent un symptôme d'autre chose, et je pense qu'un tel code peut être converti en code testable sans subir de dégradation des performances. La théorie est que ce code représente souvent des processus discontinus qui traitent périodiquement de grandes quantités de données. Si ces processus par lots devaient être convertis en petits morceaux de logique d'entreprise en temps réel qui s'exécute chaque fois que les données d'entrée sont modifiées, cette logique pourrait être exécutée sur le niveau moyen (où il peut être testé) sans prendre un coup de performance (puisque le travail est fait en petits morceaux en temps réel). Comme effet secondaire, cela élimine la longue commentaires-boucles de lot processus de gestion des erreurs. Bien sûr, cette approche ne fonctionnera pas dans tous les cas, mais il peut fonctionner dans certains cas. De plus, s'il y a des tonnes de code de base de données de traitement par lots non vérifiables dans votre système, la route vers le salut peut être longue et ardue. YMMV.

3
répondu thvo 2008-10-02 06:08:33

mais j'ai l'impression que vous êtes en fait plus concerné par la performance, qui n'est pas vraiment l'apanage des tests unitaires. Les essais unitaires sont censés être assez atomiques et visent à vérifier le comportement plutôt que la performance. Et dans ce cas, vous allez presque certainement avoir besoin de charges de classe de production afin de vérifier les plans de requête.

je pense qu'il y a deux domaines de test tout à fait distincts ici: la performance, et le véritable logique des procédures stockées.

j'ai donné l'exemple de tester la performance db dans le passé et, heureusement, nous avons atteint un point où la performance est assez bonne.

je suis tout à fait d'accord que la situation avec toute la logique commerciale dans la base de données est une mauvaise, mais c'est quelque chose que nous avons hérité avant que la plupart de nos développeurs ont rejoint l'entreprise.

Cependant, nous adoptons maintenant les services web modèle pour nos nouvelles fonctionnalités, et nous avons essayé d'éviter les procédures stockées autant que possible, en gardant la logique dans le C# code et le tir SQLCommands à la base de données (bien que linq serait maintenant la méthode préférée). Il y a encore une certaine utilisation des SPs existants, raison pour laquelle je pensais les tester rétrospectivement en unité.

2
répondu John Sibly 2008-08-15 16:53:22

vous pouvez également essayer Visual Studio for Database Professionals . Il s'agit principalement de gestion du changement, mais dispose également d'outils pour générer des données de test et des tests unitaires.

c'est assez cher tho.

2
répondu Craig 2008-08-18 09:41:19

Nous utilisons des DataFresh annulation des modifications entre chaque test, le test de sprocs est relativement facile.

ce qui manque encore, ce sont les outils de couverture du code.

1
répondu Keith 2008-08-15 16:41:34

je fais le test de l'Unité du pauvre homme. Si je suis paresseux, le test est juste quelques invocations valides avec des valeurs de paramètres potentiellement problématiques.

/*

--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....
1
répondu MatthewMartin 2009-04-29 03:32:32

LINQ simplifiera cela seulement si vous supprimez la logique de vos procédures stockées et la réimposez comme requêtes linq. Ce qui serait beaucoup plus robuste et plus facile à tester, certainement. Cependant, il semble que vos exigences l'excluent.

TL; DR: votre conception a des problèmes.

0
répondu Will 2008-08-15 15:44:00

nous testons à l'unité le code C qui appelle le SPs.

Nous avons des scripts de construction, créant des bases de données de test propres.

Et des plus grosses que nous attachons et détachons pendant le montage d'essai.

Ces tests peuvent prendre des heures, mais je pense que ça en vaut la peine.

0
répondu John Smithers 2008-08-15 15:51:21

une option pour re-factoriser le code (je vais admettre un hack laid) serait de le générer via CPP (le préprocesseur C) M4 (ne l'a jamais essayé) ou similaire. J'ai un projet qui est de faire cela et c'est en fait surtout réalisable.

le seul cas que je pense que pourrait être valable pour est 1) comme une alternative aux procédures stockées KLOC+ et 2) et ce sont mes cas, quand le but du projet est de voir jusqu'où (dans la folie) vous pouvez pousser une technologie.

0
répondu BCS 2008-08-15 17:09:23

Oh, bon sang. les sproc ne se prêtent pas aux tests (automatisés) à l'unité. Je trie-de "test de l'unité" mes sproc complexes en écrivant des tests dans les fichiers de lot T-sql et en vérifiant à la main la sortie des déclarations d'impression et les résultats.

0
répondu Booji Boy 2008-08-15 19:35:27

le problème avec le test de l'unité tout type de programmation liée aux données est que vous devez avoir un ensemble fiable de données de test pour commencer. Beaucoup dépend aussi de la complexité de la procédure stockée et ce qu'il fait. Il serait très difficile d'automatiser les tests unitaires pour un procédé très complexe qui a modifié de nombreux tableaux.

Certaines autres affiches ont noté quelques moyens simples pour automatiser manuellement les tester, et aussi quelques outils que vous pouvez utiliser avec SQL Server. Sur le Côté Oracle, le gourou PL/SQL Steven Feuerstein a travaillé sur un outil de test d'unité libre pour les procédures stockées PL/SQL appelé utPLSQL.

cependant, il a abandonné cet effort et est devenu commercial avec le testeur de code de Quest pour PL/SQL. Quest propose une version d'essai téléchargeable gratuite. Je suis sur le point de l'essayer; ma compréhension est que c'est bon à prendre soin de la tête dans la mise en place d'un cadre de test de sorte que vous pouvez vous concentrer sur juste les tests eux-mêmes, et il maintient le tests pour les réutiliser dans les tests de régression, l'un des grands avantages du développement basé sur les tests. En outre, il est censé être bon à plus que la simple vérification d'une variable de sortie et ne dispose d'une disposition pour valider les changements de données, mais je dois encore regarder de plus près moi-même. J'ai pensé que cette information pourrait être utile pour les utilisateurs D'Oracle.

0
répondu Bernard Dy 2008-08-18 00:07:13