Question

Nous avons constaté que les tests unitaires que nous avons écrits pour notre code C#/C++ ont vraiment porté leurs fruits.Mais nous avons encore des milliers de lignes de logique métier dans les procédures stockées, qui ne sont réellement testées que lorsque notre produit est déployé auprès d'un grand nombre d'utilisateurs.

Ce qui aggrave la situation, c'est que certaines de ces procédures stockées finissent par être très longues, en raison des performances affectées lors du passage de tables temporaires entre les SP.Cela nous a empêché de refactoriser pour simplifier le code.

Nous avons fait plusieurs tentatives pour créer des tests unitaires autour de certaines de nos procédures stockées clés (principalement en testant les performances), mais nous avons constaté que la configuration des données de test pour ces tests est très difficile.Par exemple, nous finissons par copier autour des bases de données de test.En plus de cela, les tests finissent par être très sensibles aux changements, et même au plus petit changement apporté à un processus stocké.ou un tableau nécessite un grand nombre de modifications des tests.Ainsi, après de nombreuses builds interrompues en raison de l’échec intermittent de ces tests de base de données, nous avons simplement dû les retirer du processus de build.

Donc, l'essentiel de mes questions est le suivant :Quelqu'un a-t-il déjà écrit avec succès des tests unitaires pour ses 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 qu'au lieu d'avoir à créer des tableaux de données de test, vous pourriez simplement créer une collection d'objets de test et tester votre code Linq dans une situation « Linq to Objects » ?(Je suis totalement nouveau sur Linq, donc je ne sais pas si cela fonctionnerait du tout)

Était-ce utile?

La solution

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

Cela semblait mieux que l'habituel "exécuter un script pour configurer ma base de données de test, puis après l'exécution des tests, effectuer un nettoyage des données indésirables/de test".Cela semblait également plus proche des tests unitaires, car ces tests pouvaient être exécutés seuls sans avoir beaucoup de "tout dans la base de données doit être" juste "avant d'exécuter 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

vous verrez ensuite un exemple de classe d'accès aux données 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 également hériter d'une classe de base très simple qui effectue votre travail de configuration/restauration - ou conserver cela sur une base de test unitaire.

ci-dessous se trouve la classe de base de test simple que j'ai utilisée

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 - ce qui suit est un test simple utilisant cette classe de base de test qui montre comment tester l'ensemble du cycle CRUD pour vous assurer que tous les sprocs font leur travail et que votre code ado.net effectue correctement le mappage gauche-droite.

Je sais que cela ne teste pas la procédure "spGetProducts" utilisée dans l'exemple d'accès aux données ci-dessus, mais vous devriez voir la puissance derrière cette approche des procédures de test unitaire.

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 cela m'a aidé d'avoir une classe réutilisable pour le travail d'accès aux données, et encore une autre classe réutilisable pour mes tests afin que je n'aie pas à faire le travail de configuration/démontage encore et encore ;)

Autres conseils

As-tu essayé UnitéDB?Il est conçu pour tester unitairement votre base de données, et uniquement votre base de données, sans avoir besoin de passer par votre code C#.

Si vous réfléchissez au type de code que les tests unitaires ont tendance à promouvoir :petites routines hautement cohérentes et faiblement couplées, alors vous devriez être en mesure de voir où pourrait se situer au moins une partie du problème.

Dans mon monde cynique, les procédures stockées font partie de la tentative de longue date du monde des SGBDR pour vous persuader de déplacer vos traitements professionnels dans la base de données, ce qui est logique si l'on considère que les coûts de licence de serveur ont tendance à être liés à des éléments tels que le nombre de processeurs.Plus vous exécutez d’éléments dans votre base de données, plus ils en tirent profit.

Mais j'ai l'impression que vous êtes en réalité plus préoccupé par les performances, ce qui n'est pas du tout l'apanage des tests unitaires.Les tests unitaires sont censés être assez atomiques et visent à vérifier le comportement plutôt que les performances.Et dans ce cas, vous aurez presque certainement besoin de charges de classe production pour vérifier les plans de requête.

Je pense que vous avez besoin d'une classe différente d'environnement de test.Je suggérerais une copie de production comme la plus simple, en supposant que la sécurité n'est 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 leur donnera un bon test comme effet secondaire) et exécutez vos timings.

Quelque chose comme ca.

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

Je dois voter pour favoriser fortement les procédures stockées et placer votre logique métier là où je (et la plupart des administrateurs de base de données) pense qu'elle appartient, dans la base de données.

Je sais qu'en tant qu'ingénieurs logiciels, nous souhaitons qu'un code magnifiquement refactorisé, écrit dans notre langage préféré, contienne toute notre logique importante, mais les réalités des performances dans les systèmes à volume élevé et la nature critique de l'intégrité des données nous obligent à faire des compromis. .Le code SQL peut être laid, répétitif et difficile à tester, mais je ne peux pas imaginer la difficulté de régler une base de données sans avoir un contrôle total sur la conception des requêtes.

Je suis souvent obligé de repenser complètement les requêtes, d'inclure des modifications dans le modèle de données, pour que les choses s'exécutent dans un délai acceptable.Avec les procédures stockées, je peux garantir que les modifications seront transparentes pour l'appelant, car une procédure stockée offre une excellente encapsulation.

Je suppose que vous souhaitez des tests unitaires dans MSSQL.En regardant DBUnit, il existe certaines limitations dans sa prise en charge de MSSQL.Il ne prend pas en charge NVarChar par exemple. Voici quelques vrais utilisateurs et leurs problèmes avec DBUnit.

Bonne question.

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

Il existe de nombreuses 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#, j'ai donc 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, c'est de décoller avec quelques tester, et partir de là.Cette approche signifie que même si ma couverture est faible, je teste certains processus stockés en même temps que je teste le code qui les appellera.Il y a une certaine logique à cela.

Je suis exactement dans la même situation que l'affiche originale.Cela se résume à la performance par rapport à la testabilité.Mon parti pris est en faveur de la testabilité (le faire fonctionner, le faire correctement, le rendre rapide), ce qui suggère de garder la logique métier en dehors de la base de données.Non seulement les bases de données ne disposent pas des cadres de test, des constructions de factorisation de code et des outils d'analyse et de navigation de code que l'on trouve dans des langages comme Java, mais le code de base de données hautement factorisé est également lent (là où le code Java hautement factorisé ne l'est pas).

Cependant, je reconnais la puissance du traitement des ensembles de bases de données.Lorsqu'il est utilisé de manière appropriée, SQL peut réaliser des tâches incroyablement puissantes avec très peu de code.Donc, je suis d'accord avec une certaine logique basée sur des ensembles vivant dans la base de données, même si je ferai toujours tout ce que je peux pour la tester unitairement.

Dans le même ordre d'idées, il semble qu'un code de base de données très long et procédural soit souvent le symptôme de quelque chose d'autre, et je pense qu'un tel code peut être converti en code testable sans entraîner de baisse de performances.La théorie est qu'un tel code représente souvent des processus par lots qui traitent périodiquement de grandes quantités de données.Si ces processus par lots devaient être convertis en morceaux plus petits de logique métier en temps réel qui s'exécutent chaque fois que les données d'entrée sont modifiées, cette logique pourrait être exécutée au niveau intermédiaire (où elle peut être testée) sans nuire aux performances (puisque le travail est effectué par petits morceaux en temps réel).Comme effet secondaire, cela élimine également les longues boucles de rétroaction liées à la gestion des erreurs des processus par lots.Bien entendu, cette approche ne fonctionnera pas dans tous les cas, mais elle peut fonctionner dans certains cas.De plus, s’il existe des tonnes de codes de base de données de traitement par lots non testables dans votre système, le chemin vers le salut peut être long et ardu.YMMV.

Mais j'ai l'impression que vous êtes en réalité plus préoccupé par les performances, ce qui n'est pas du tout l'apanage des tests unitaires.Les tests unitaires sont censés être assez atomiques et visent à vérifier le comportement plutôt que les performances.Et dans ce cas, vous aurez presque certainement besoin de charges de classe production pour vérifier les plans de requête.

Je pense qu'il y a ici deux domaines de test bien distincts :les performances et la logique réelle des procédures stockées.

J'ai donné l'exemple du test des performances de la base de données dans le passé et, heureusement, nous avons atteint un point où les performances sont suffisamment bonnes.

Je suis tout à fait d'accord que la situation de toute la logique métier de la base de données est mauvaise, mais c'est quelque chose dont nous avons hérité avant que la plupart de nos développeurs ne rejoignent l'entreprise.

Cependant, nous adoptons désormais le modèle de services Web pour nos nouvelles fonctionnalités et nous essayons d'éviter autant que possible les procédures stockées, en conservant la logique du code C# et en lançant des commandes SQL sur la base de données (même si Linq serait désormais la méthode préférée).Les SP existants sont encore utilisés dans une certaine mesure, c'est pourquoi je pensais à les tester unitairement rétrospectivement.

Vous pouvez également essayer Visual Studio pour les professionnels des bases de données.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 quand même assez cher.

Nous utilisons DonnéesFresh pour annuler les modifications entre chaque test, tester les sprocs est relativement simple.

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

Je fais des tests unitaires du pauvre.Si je suis paresseux, le test consiste simplement en 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....

LINQ ne simplifiera cela que si vous supprimez la logique de vos procédures stockées et la réimplémentez sous forme de requêtes Linq.Ce qui serait certainement beaucoup plus robuste et plus facile à tester.Cependant, il semble que vos exigences l'empêchent.

TL;DR :Votre conception présente des problèmes.

Nous testons unitairement le code C# qui appelle les SP.
Nous avons des scripts de construction, créant des bases de données de test propres.
Et les plus gros, nous attachons et détachons pendant le montage de test.
Ces tests pourraient prendre des heures, mais je pense que cela en vaut la peine.

Une option pour refactoriser le code (j'admets un vilain hack) serait de le générer via CPP (le préprocesseur C) M4 (je ne l'ai jamais essayé) ou similaire.J'ai un projet qui fait exactement cela et il est en fait pratiquement réalisable.

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

Oh mec.Les sprocs ne se prêtent pas aux tests unitaires (automatisés).Je "teste unitairement" mes sprocs complexes en écrivant des tests dans des fichiers batch t-sql et en vérifiant manuellement la sortie des instructions d'impression et les résultats.

Le problème avec les tests unitaires de tout type de programmation liée aux données est que vous devez disposer d'un ensemble fiable de données de test pour commencer.Beaucoup dépend aussi de la complexité du processus stocké et de ce qu'il fait.Il serait très difficile d’automatiser les tests unitaires pour une procédure très complexe modifiant de nombreuses tables.

Certaines des autres affiches ont indiqué des moyens simples d'automatiser leurs tests manuels, ainsi que certains outils que vous pouvez utiliser avec SQL Server.Du côté d'Oracle, le gourou du PL/SQL Steven Feuerstein a travaillé sur un outil de test unitaire gratuit pour les procédures stockées PL/SQL appelé utPLSQL.

Cependant, il a abandonné cet effort et est ensuite devenu commercial avec Code Tester de Quest pour PL/SQL.Quest propose une version d'essai téléchargeable gratuitement.Je suis sur le point de l'essayer ;je crois comprendre qu'il est efficace pour gérer les frais généraux liés à la mise en place d'un cadre de test afin que vous puissiez vous concentrer uniquement sur les tests eux-mêmes, et qu'il conserve les tests afin que vous puissiez les réutiliser dans les tests de régression, l'un des grands avantages de développement piloté par les tests.De plus, il est censé faire plus que simplement vérifier une variable de sortie et permet de valider les modifications des données, mais je dois quand même y regarder de plus près moi-même.J'ai pensé que cette information pourrait être utile aux utilisateurs d'Oracle.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top