Test du code SQL Server avec tSQLt

FYI: Cet article est une version développée de mon rapport sur SQA Days # 25.

Sur la base de mon expérience de communication avec des collègues, je peux dire que tester du code dans une base de données n'est pas une pratique courante. Cela peut être un danger potentiel. La logique de la base de données est écrite par les mêmes personnes qui écrivent le code "normal". Par conséquent, des erreurs peuvent également y être présentes, et elles peuvent également entraîner des conséquences négatives pour le produit, l'entreprise et les consommateurs. Peu importe qu'il s'agisse de procédures stockées qui aident le backend ou d'ETL qui convertissent des données en stockage - il y a un risque et les tests peuvent le réduire considérablement. Je veux vous dire ce qu'est tSQLt et comment il nous aide à tester le code dans SQL Server.



Contexte


Il existe un grand entrepôt sur SQL Server contenant diverses données de recherche clinique. Il est rempli à partir de diverses sources (principalement des bases de données documentaires). À l'intérieur du serveur lui-même, les données sont converties à plusieurs reprises à l'aide d'ETL. À l'avenir, ces données peuvent être téléchargées dans des bases de données plus petites pour être utilisées par des applications Web qui résolvent certains petits problèmes spécifiques. Certains clients du client demandent également une API pour leurs besoins internes. Dans la mise en œuvre de ces API, les procédures stockées et les requêtes sont souvent utilisées.


En général, le code est du côté du SGBD dans l'ordre.

Pourquoi tout cela est nécessaire


Comme déjà compris depuis l'introduction, le code dans la base de données est le même code
comme les autres, et il peut également y avoir des erreurs.

Je pense que beaucoup de gens sont conscients de la dépendance du prix du bug au moment de sa découverte, dont la découverte est généralement attribuée à Barry Bohem. Une erreur commise à un stade précoce et détectée à un stade ultérieur peut être plus coûteuse en raison de la nécessité de passer par plusieurs étapes (codage, unité, intégration, test du système, etc.) à la fois pour localiser l'erreur et ramener le code corrigé dans l'étape à laquelle le problème a été identifié. Cet effet est également pertinent pour le cas d'entrepôt. Si une erreur s'est glissée dans certains ETL et que les données subissent plusieurs transformations, alors si une erreur est détectée dans les données, vous aurez:

  1. Suivez toutes les étapes de conversion pour localiser le problème.
  2. Résolvez le problème.
  3. Récupérez les données corrigées (les corrections manuelles ne sont pas exclues).
  4. Vérifiez que les données incorrectes provoquées par l'erreur n'apparaissent pas ailleurs.

N'oubliez pas que nous ne vendons pas de peluches. Une erreur dans un domaine comme la recherche clinique peut nuire non seulement aux entreprises, mais aussi à la santé des personnes.

Comment tester?


Puisque nous parlons de tests de code, nous entendons les tests unitaires et d'intégration. Ces choses sont très répétitives et impliquent une régression constante. À strictement parler, personne ne procède à de tels tests manuellement (enfin, peut-être à l'exception de certains cas complètement dégénérés).

Un bon bonus: les tests peuvent être un matériau auxiliaire lors de la documentation du code. Soit dit en passant, les exigences des clients peuvent ressembler à ceci (cliquable):


Excel, deux colonnes avec exigences + informations de support dispersées dans d'autres colonnes + balisage flou, ce qui est plus déroutant qu'utile. Si nécessaire, restaurer les souhaits d'origine peut être difficile. Les tests peuvent aider à saisir plus précisément les nuances de l'implémentation (bien sûr, vous ne devez pas les considérer comme l'équivalent de la documentation).

Malheureusement, avec la complexité du code, la complexité des tests augmente et cet effet peut être nivelé.

Les tests peuvent servir de couche de sécurité supplémentaire contre les morses spontanés. Les auto-tests en CI en raison du formalisme aident à faire face à ce problème.

Si notre choix s'est porté sur la voie de l'automatisation, alors nous devons décider des outils pour sa mise en œuvre.

Comment tester?


Dans le cas du test du code dans la base de données, je distingue deux approches: propulsée par SQL, c'est-à-dire fonctionnant directement dans le SGBD et non propulsée par SQL. J'ai pu mettre en évidence les nuances suivantes:
Alimenté par SQL
Non alimenté par SQL
Nécessite l'installation d'objets dans la base de données
L'installation d'outils externes supplémentaires dans la base de données est requise
Les tests sont toujours indépendants des technologies utilisées dans l'application en dehors de la base de données
Les tests peuvent dépendre des technologies utilisées en dehors de la base de données.
Le cadre est toujours lié à un SGBD spécifiqueLe cadre prend souvent en charge plusieurs SGBD.
Pour écrire des tests, seule la connaissance du SGBD est requise; pour le développement, vous pouvez utiliser des testeurs manuels ou DBAL'écriture de tests nécessite généralement une connaissance supplémentaire de tout langage et / ou technologie de programmation; ont souvent besoin de l'aide de programmeurs
L'exécution au niveau du SGBD permet l'utilisation de contrefaçons et d'assertions plus avancées
L'exécution externe peut limiter les capacités de l'outil
Dans SQL Server, nous avons le choix:
Informations générales
Le titreL'approcheL'architectureÉcrit leTests pour
tSQLtAlimenté par SQLxUnitT-SQL + CLRT-sql
TSQLUnitAlimenté par SQLxUnitT-sqlT-sql
utTSQLAlimenté par SQLxUnitT-sqlT-sql
TstAlimenté par SQLxUnitT-sqlT-sql
DbfitNon alimenté par SQLFitnessessC # / javaDémarque wiki
SlackerNon alimenté par SQLRSpec (orienté BDD)RubisRubis
NUnit , etc.Non alimenté par SQLxUnitN / aN / a
Les dates
Le titrePremière apparitionDernier commitDernière version
tSQLt2007-07-2707/07/201931/01/2016
TSQLUnit16/12/2006 (0,9)
2007-07-21 (0,91 rc1)
26/04/2018 (GitHub)04/09/2011 (SourceForge)
utTSQL2003-03-122003-03-122003-03-12
Tst02-03-2009 (v1.0)N / a30-03-2012
Dbfit12-01-200910-09-201815/08/2015
Slacker23/06/201112-12-201812-12-2018
NUnit , etc.N / aN / aN / a
Les possibilités
Le titreCLR non requisSortie XMLDes tests enveloppés dans une transactionFauxGestionnaires d'erreursAssertions
tSQLt-++++Super
TSQLUnit+-+--Très mauvais
utTSQL+----Mauvais
Tst+++ (opt.)-+Super
Dbfit+-+ (opt.)-+Bon; il y a des nuances
Slacker+-+ (opt.)--Bon; il y a des nuances
NUnit , etc.++N / aN / aN / aSuper il y a des nuances
Autre
Le titreLa documentationCommunauté
tSQLtSuper il y a des nuancesSuper
TSQLUnitMauvaisMauvais
utTSQLSuperMauvais
TstSuperMauvais
DbfitSuperOk
SlackerSuperOk
NUnit , etc.SuperSuper
Les évaluations de «bon-mauvais» sont subjectives, désolé, sans cela, nulle part.

Explication: La «première apparition» est la date la plus ancienne du chemin de vie du framework que j'ai réussi à trouver, c'est-à-dire la première version ou commit.

Vous remarquerez peut-être que les alternatives basées sur SQL ont été abandonnées depuis un certain temps, et tSQLt est la seule option prise en charge. Fonctionnellement, tSQLt gagne également. La seule chose est qu'en termes d'assertions, TST propose un choix légèrement plus riche que tSQLt, qui, cependant, est peu susceptible de l'emporter sur ses inconvénients.

Il y a des nuances dans la documentation tSQLt, mais j'en parlerai plus tard.

Dans le monde non propulsé par SQL, les choses ne sont pas aussi simples. Des alternatives, bien que non super actives, sont en cours de développement. DbFit est un outil intéressant basé sur le framework FitNesse. Il propose des tests d'écriture sur le balisage wiki. Slacker est également une chose curieuse - l'approche BDD lors de l'écriture de tests pour la base de données.

Je vais discuter des assertions non propulsées par SQL. Extérieurement, il y en a moins, et on pourrait dire qu'ils sont pires à cause de cela. Mais ici, il vaut la peine de considérer qu'ils sont fondamentalement différents de tSQLt. Tout n'est pas si simple.

La dernière ligne est «NUnit, etc.» - c'est plutôt un rappel. De nombreux cadres de tests unitaires familiers dans le travail quotidien peuvent être utilisés sur des bases de données auxiliaires à l'aide de bibliothèques auxiliaires. Le tableau a beaucoup de N / A, car cette ligne, en fait, comprend de nombreux outils. Les «nuances» dans la colonne d'assertion viennent du même point - dans différents outils, leur ensemble peut varier et la question de l'applicabilité à la base de données est ouverte.

Comme autre mesure intéressante, nous pouvons considérer les tendances de Google .


Nuances:

  1. Je n'ai pas inclus Slacker, car ce nom peut signifier beaucoup de choses (et les demandes comme «Slacker framework» ne sont pas particulièrement visibles sur les graphiques).
  2. Par curiosité, la tendance TST a été ajoutée, mais elle ne reflète pas non plus beaucoup la situation, car il s'agit d'une abréviation qui signifie beaucoup de choses différentes.
  3. Je n'ai pas inclus NUnit et ses analogues, car il s'agissait à l'origine de frameworks pour tester le code des applications elles-mêmes, et leurs tendances ne sont pas représentatives de notre contexte.

En général, nous pouvons dire que tSQLt se présente favorablement dans le contexte des analogues.

Qu'est-ce que tSQLt?


tSQLt, comme vous pouvez le deviner, est un framework de tests unitaires basé sur SQL.

Site officiel

La prise en charge de SQL Server est annoncée depuis 2005 SP2. Je n'ai jamais pu regarder aussi loin dans le passé, mais nous avons 2012 sur le serveur de développement, j'ai localement 2017 - il n'y a eu aucun problème.

Open source, une licence Apache 2.0, est disponible sur GitHub . Vous pouvez bifurquer, passer en contrebande, utiliser gratuitement dans des projets commerciaux et, surtout, ne pas avoir peur des signets dans le CLR.

Mécanique du travail



Les cas de test sont des procédures stockées. Ils sont combinés en classes de test (suite de tests en termes de xUnit).

Les classes de test ne sont rien de plus que des schémas de base de données réguliers. Ils diffèrent des autres régimes par leur inscription dans les tableaux-cadres. Vous pouvez effectuer un tel enregistrement en appelant une procédure - tSQLt.NewTestClass.

À l'intérieur de la classe de test, il est également possible de définir une procédure SetUp qui sera exécutée avant l'exécution de chaque scénario de test individuel.

Une procédure de démontage pour restaurer le système à la fin du scénario de test n'est pas nécessaire. Chaque scénario de test ainsi que la procédure de configuration sont exécutés dans une transaction distincte, qui est annulée après la collecte des résultats. C'est très pratique, mais cela a des effets négatifs, dont je parlerai ci-dessous.

Le cadre vous permet d'exécuter des cas de test individuels, des classes de test entières ou toutes les classes de test enregistrées à la fois.

Caractéristiques par exemple


N'ayant pas envie de raconter un guide officiel déjà simple, je vais parler des possibilités du framework à l'aide d'exemples.

Avertissement:

  • les exemples sont simplifiés;
  • dans l'original, tout le code de test n'a pas été écrit par moi, ce sont plutôt les fruits de la créativité collective;
  • L'exemple 2 a été inventé afin de démontrer plus complètement les capacités de tSQLt.

Exemple 1: CsvSql


À la demande de l'un des clients du client, les éléments suivants ont été mis en œuvre. La base de données dans les champs Nvarchar (MAX) stocke les requêtes SQL. Pour afficher ces requêtes, une interface minimale est boulonnée. Les jeux de résultats renvoyés par ces requêtes sont utilisés par le backend pour générer le fichier CSV à renvoyer via l'appel API.


Les jeux de résultats sont assez lourds et contiennent de nombreuses colonnes. Un exemple conditionnel d'un tel ensemble de résultats:


Cet ensemble de résultats comprend des données d'essais cliniques. Examinons de plus près comment ClinicsNum est considéré - le nombre de cliniques impliquées dans l'étude. Nous avons deux tableaux: [Trial] et [Clinic]:


Il y a FK: [Clinic]. [TrialID] -> [Trial]. [TrialID]. Évidemment, pour calculer le nombre de cliniques, nous avons juste besoin du COUNT habituel (*).

SELECT COUNT(*), ...  FROM dbo.Trial  LEFT JOIN dbo.Clinic    ON Trial.ID = Clinic.TrialID  WHERE Trial.Name = @trialName  GROUP BY ... 

Comment testons-nous une telle demande? Pour commencer, nous pouvons utiliser le talon pratique - FakeTable, qui simplifiera considérablement le travail ultérieur.

 EXEC tSQLt.FakeTable 'dbo.Trial'; EXEC tSQLt.FakeTable 'dbo.Clinic'; 

FakeTable fait une chose simple: renommer les anciennes tables et en créer de nouvelles à leur place. Les mêmes noms, les mêmes colonnes, mais sans contrainte'ov et trigger'ov.

Pourquoi avons-nous besoin de cela:

  1. Certaines données de la base de données de test peuvent interférer avec les tests. Grâce à FakeTable, nous ne dépendons pas d'eux.
  2. Pour le test, en règle générale, vous ne devez remplir que quelques colonnes. Il peut y en avoir beaucoup dans le tableau, et certains d'entre eux auront des contraintes. De cette façon, nous simplifions la poursuite de l'installation des données de test - nous n'insérerons que celles qui sont vraiment nécessaires pour le scénario de test.
  3. Nous n'affecterons certainement aucun déclencheur lors de l'insertion de données de test et nous n'avons pas à nous soucier des post-effets indésirables.

Ensuite, insérez les données de test dont nous avons besoin:

 INSERT INTO dbo.Trial ([ID], [Name]) VALUES (1,   'Valerian'); INSERT INTO dbo.Clinic ([ID], [TrialID], [Name]) VALUES (1,   1,        'Clinic1'), (2,   1,        'Clinic2'); 

Nous obtenons notre requête de la base de données, créons la table réelle et la remplissons avec le jeu de résultats de notre requête:

 DECLARE @sqlStatement NVARCHAR(MAX) = (SELECTCREATE TABLE actual ([TrialID], ...); INSERT INTO actual EXEC sp_executesql @sqlStatement, ... 

Remplir attendu - valeurs attendues:

 CREATE TABLE expected (   ClinicsNum INT ); INSERT INTO expected SELECT 2 

Je veux attirer votre attention sur le fait que dans le tableau Expected, nous n'avons qu'une seule colonne, alors que dans Actual, nous en avons un ensemble complet.


Cela est dû à la fonctionnalité de la procédure AssertEqualsTable, que nous utiliserons pour vérifier les valeurs.

 EXEC tSQLt.AssertEqualsTable   'expected',   'actual',   'incorrect number of clinics'; 

Il compare uniquement les colonnes présentes dans les deux tables comparées. C'est très pratique dans notre cas, car la requête de test renvoie un grand nombre de colonnes, chacune ayant sa propre logique, parfois très déroutante. Nous ne voulons pas gonfler les cas de test, donc cette fonctionnalité nous est très utile. Bien sûr, c'est une épée à double tranchant. Si dans Réel, un ensemble de colonnes est rempli automatiquement via SELECT TOP 0 et à un moment donné, une colonne supplémentaire apparaît soudainement, alors un tel cas de test ne rattrapera pas ce moment. Pour gérer de telles situations, vous devez effectuer des vérifications supplémentaires.

Procédures soeurs AssertEqualsTable


Il convient de mentionner que tSQLt contient deux procédures similaires à AssertEqualsTable. Ce sont AssertEqualsTableSchema et AssertResultSetsHaveSameMetaData. Le premier fait la même chose que AssertEqualsTable, mais sur les métadonnées de la table. La seconde fait une comparaison similaire, mais sur les métadonnées des jeux de résultats.

Exemple 2: contraintes


Dans l'exemple précédent, nous avons vu comment vous pouvez supprimer la contrainte. Mais que faire si nous devons les vérifier? Techniquement, cela fait également partie de la logique et peut également être considéré comme un candidat pour la couverture des tests.

Considérez la situation de l'exemple précédent. Deux tables - [Trial] et [Clinic]; [TrialID] FK:


Essayons d'écrire un cas de test pour tester cette contrainte. Au début, comme la dernière fois, nous simulons des tables.

 EXEC tSQLt.FakeTable '[dbo].[Trial]' EXEC tSQLt.FakeTable '[dbo].[Clinic]' 

L'objectif est le même: se débarrasser des restrictions inutiles. Nous voulons des chèques isolés sans gestes inutiles.

Ensuite, nous renvoyons la contrainte dont nous avons besoin à l'endroit à l'aide de la procédure ApplyConstraint:

 EXEC tSQLt.ApplyConstraint   '[dbo].[Clinic]',   'Trial_FK'; 

Ici, nous avons mis en place une configuration pratique pour la vérification directe. Le contrôle lui-même consistera en ce qu'une tentative d'insertion de données entraînera inévitablement une exception. Pour que le scénario de test fonctionne correctement, nous devons intercepter cette exception. Le gestionnaire d'exceptions ExpectException nous y aidera.

 EXEC tSQLt.ExpectException   @ExpectedMessage = 'The INSERT statement conflicted...',   @ExpectedSeverity = 16,   @ExpectedState = 0; 

Après avoir installé le gestionnaire, vous pouvez essayer d'insérer le non insérable.

 INSERT INTO [dbo].[Clinic] ([TrialID])   VALUES (1) 

Exception interceptée. Test réussi.

Procédures soeurs ApplyConstraint


Les développeurs TSQLt nous proposent une approche similaire pour tester les déclencheurs. Vous pouvez utiliser la procédure ApplyTrigger pour renvoyer un déclencheur à la fausse table. De plus, tout est comme dans l'exemple ci-dessus - nous déclenchons le déclencheur, vérifions le résultat.

ExpectNoException - l'antonyme d'ExpectException


Pour les cas où une exception ne devrait certainement pas se produire, il existe une procédure ExpectNoException. Il fonctionne de la même manière qu'une ExpectException, sauf que le test est considéré comme ayant échoué si une exception se produit.

Exemple 3: sémaphore


La situation est la suivante. Il existe un certain nombre de procédures stockées et de services Windows. Le début de leur exécution peut être provoqué par divers événements externes. Dans ce cas, l'ordre admissible de leur exécution est fixe. Un sémaphore est utilisé pour différencier l'accès aux tables de base de données. Il s'agit d'un groupe de procédures stockées.

Par exemple, considérez l'une de ces procédures. Nous avons deux tableaux:


Le tableau [Processus] contient une liste des processus autorisés à être exécutés, [ProcStatus] - une liste des statuts de ces processus.

Que fait notre procédure? Lorsqu'il est appelé, une série de vérifications se produit d'abord:

  1. Le nom du processus à démarrer, passé dans le paramètre de procédure, est recherché dans le champ [Nom] de la table [Processus].
  2. Si le nom du processus a été trouvé, il est vérifié s'il est actuellement possible de le démarrer - l'indicateur [IsRunable] de la table [Process].
  3. S'il s'est avéré que le processus est acceptable pour l'exécution, alors il reste à être sûr qu'il n'est pas déjà en cours d'exécution. Dans le tableau [ProcStatus], l'absence d'enregistrements sur ce processus avec le statut = 'InProg' est vérifiée.

Si tout va bien, un nouvel enregistrement sur ce processus avec le statut «InProg» est ajouté à ProcStatus (cela est considéré comme un lancement), l'ID de cet enregistrement est renvoyé avec le paramètre ProcStatusId. Si une vérification échoue, nous nous attendons à ce qui suit:

  1. Une lettre est envoyée à l'administrateur système.
  2. Renvoie ProcStatusId = -1.
  3. Une nouvelle entrée dans [ProcStatus] n'est pas ajoutée.

Écrivons un cas de test pour tester le cas lorsque le processus ne figure pas dans la liste des cas acceptables.

Pour plus de commodité, appliquez immédiatement FakeTable. Ici, ce n'est pas si fondamentalement important, mais cela peut être utile:

  1. Nous sommes garantis de nous débarrasser de toutes les données qui pourraient interférer avec la bonne exécution du cas de test.
  2. Nous simplifierons la vérification des entrées manquantes dans ProcStatus.

 EXEC tSQLt.FakeTable 'dbo.Process'; EXEC tSQLt.FakeTable 'dbo.ProcStatus'; 

Pour envoyer un message, la procédure [SendEmail] écrite par nos programmeurs est utilisée. Pour vérifier l'envoi d'une lettre aux administrateurs, nous devons intercepter son appel. Dans ce cas, tSQLt nous propose d'utiliser SpyProcedure.

 EXEC tSQLt.SpyProcedure 'dbo.SendEmail' 

SpyProcedure effectue les opérations suivantes:

  1. Crée une table de la forme [dbo]. [SendEmail_SpyProcedureLog].
  2. Comme FakeTable, il remplace la procédure d'origine par la sienne, avec le même nom, mais contenant une logique de journalisation. Si vous le souhaitez, vous pouvez ajouter votre propre logique.

Comme vous pouvez le deviner, la journalisation se produit dans la table [dbo]. [SendEmail_SpyProcedureLog]. Ce tableau contient la colonne [_ID_] - le numéro de séquence de l'appel de procédure. Les colonnes suivantes portent les noms des paramètres passés à la procédure et les valeurs passées dans les appels y sont collectées. Si nécessaire, ils peuvent également être vérifiés.


La dernière chose que nous devons faire avant d'appeler le sémaphore et de vérifier est de créer une variable dans laquelle nous mettrons l'ID d'enregistrement de la table [ProcStatus] (plus précisément, -1, car l'enregistrement ne sera pas ajouté).

 DECLARE @ProcStatusId BIGINT; 

Appelez le sémaphore:

 EXEC dbo.[Semaphore_JobStarter]   'SomeProcess',   @ProcStatusId OUTPUT; --    -1 

Voilà, nous avons maintenant toutes les données nécessaires pour la vérification. Commençons par vérifier l'envoi.
lettres:

 IF NOT EXISTS (   SELECT *   FROM dbo.SendEmail_SpyProcedureLog) EXEC tSQLt.Fail 'SendEmail has not been run.'; 

Dans ce cas, nous avons décidé de ne pas vérifier les paramètres transmis lors de l'envoi, mais simplement de vérifier le fait lui-même. J'attire votre attention sur la procédure tSQLt.Fail. Il vous permet d'échouer "officiellement" le cas de test. Si vous avez besoin de construire une construction spécifique, tSQLt.Fail vous permettra de le faire.

Ensuite, vérifiez l'absence d'entrées dans [dbo]. [ProcStatus]:

 EXEC tSQLt.AssertEmptyTable 'dbo.ProcStatus'; 

C'est là que la FakeTable que nous avons appliquée au tout début nous a aidés. Grâce à lui, nous pouvons nous attendre au vide. Sans cela, pour une vérification précise, nous devrions, dans le bon sens, comparer le nombre d'enregistrements avant et après le sémaphore.

Equality ProcStatusId = -1, nous pouvons facilement vérifier avec AssertEquals:

 EXEC tSQLt.AssertEquals   -1,       @ProcStatusId,       'Wrong ProcStatusId.'; 

AssertEquals est minimaliste - il compare simplement deux valeurs, rien de surnaturel.

Procédures de frères et sœurs AssertEquals


Pour comparer les valeurs, nous disposons de plusieurs procédures:

  • AssertEquals
  • AssertNotEquals
  • AssertEqualsString
  • Assertike

Je pense que leurs noms parlent d'eux-mêmes. La seule chose à noter est l'existence d'une procédure AssertEqualsString distincte. Le fait est que AssertEquals / AssertNotEquals / AssertLike fonctionne avec SQL_VARIANT, et NVARCHAR (MAX) ne s'applique pas à cela, et donc les développeurs tSQLt ont dû allouer une procédure distincte pour tester NVARCHAR (MAX).

Fausse fonction


FakeFunction avec un certain étirement peut être appelé une procédure similaire à SpyProcedure. Ce faux vous permet de remplacer n'importe quelle fonction par la plus simple nécessaire. Étant donné que les fonctions de SQL Server fonctionnent sur le principe d'un tube avec du dentifrice - elles donnent le résultat par le "seul trou technologique", alors, malheureusement, aucune fonctionnalité de journalisation n'est fournie. Seulement un remplacement pour la logique.

Pièges


Il convient de noter certains pièges que vous pouvez rencontrer en travaillant avec tSQLt. Dans ce cas, par écueils, j'entends certains problèmes problématiques nés en raison des limitations de SQL Server et / ou qui ne peuvent pas être résolus par les développeurs du framework.

Annulation / corruption de transactions


Le premier et le plus important problème auquel notre équipe a été confrontée a été l'annulation de transactions. SQL Server ne sait pas comment restaurer séparément les transactions imbriquées - uniquement tout dans son ensemble, jusqu'aux plus externes. Étant donné que tSQLt encapsule chaque cas de test dans une transaction distincte, cela devient un problème. Après tout, une restauration de transaction à l'intérieur de la procédure de test peut interrompre l'exécution du test, provoquant une erreur d'exécution non descriptive.

Pour contourner ce problème, nous utilisons des points de sauvegarde. L'idée est simple. Avant de démarrer une transaction dans la procédure de test, nous vérifions si nous sommes déjà à l'intérieur de la transaction. S'il s'avère que oui, alors nous supposons qu'il s'agit d'une transaction tSQLt, mettez savepoint au lieu de démarrer. Ensuite, si nécessaire, nous retournerons à ce point de sauvegarde, et non au début de la transaction. L'imbrication en tant que telle ne l'est pas.


Le problème est compliqué par la corruption des transactions. Si soudainement quelque chose s'est mal passé et que l'exception a fonctionné, alors la transaction peut devenir vouée à l'échec. Une telle transaction peut non seulement être validée, mais également annulée dans savepoint, mais uniquement annulée.

Compte tenu de tout ce qui précède, vous devez appliquer la conception suivante:

 DECLARE @isNestedTransaction BIT =   CASE WHEN @@trancount > 0       THEN 'true'       ELSE 'false' END; BEGIN TRY   IF @isNestedTransaction = 'false'       BEGIN TRANSACTION   ELSE       SAVE TRANSACTION SavepointName;       -- something useful   IF @isNestedTransaction = 'false'   COMMIT TRANSACTION; END TRY BEGIN CATCH   DECLARE @isCommitable BIT =       CASE WHEN XACT_STATE() = 1           THEN 'true'           ELSE 'false'   END;   IF @isCommitable = 'true' AND @isNestedTransaction = 'true'       ROLLBACK TRANSACTION SavepointName;   ELSE       ROLLBACK;   THROW; END CATCH; 

Considérez le code en plusieurs parties. Nous devons d'abord déterminer si nous sommes à l'intérieur d'une transaction:

 DECLARE @isNestedTransaction BIT =   CASE WHEN @@trancount > 0       THEN 'true'       ELSE 'false' END; 

Après avoir reçu l'indicateur @isNestedTransaction, exécutez le bloc TRY et définissez le point de sauvegarde ou le début de la transaction, selon la situation.

 BEGIN TRY   IF @isNestedTransaction = 'false'       BEGIN TRANSACTION   ELSE       SAVE TRANSACTION SavepointName;       -- something useful 

Après avoir fait quelque chose d'utile, validez, s'il s'agit d'un «vrai» début de la procédure.

        -- something useful   IF @isNestedTransaction = 'false'   COMMIT TRANSACTION; END TRY 

Bien sûr, s'il s'agit d'un lancement à partir d'un scénario de test, nous n'avons besoin de rien engager. À la fin de l'exécution, tSQLt annulera simplement toutes les modifications. Si soudainement quelque chose s'est mal passé et que nous sommes entrés dans le bloc CATCH, la première chose à faire est de savoir si notre transaction peut même être validée.

 BEGIN CATCH   DECLARE @isCommitable BIT =       CASE WHEN XACT_STATE() = 1           THEN 'true'           ELSE 'false'   END; 

Nous ne pouvons revenir au point de sauvegarde que si

  1. transaction commitable
  2. un essai a lieu, c.-à-d. le point de sauvegarde existe.

Dans d'autres cas, nous devons annuler la totalité de la transaction.

    IF @isCommitable = 'true' AND @isNestedTransaction = 'true'       ROLLBACK TRANSACTION SavepointName;   ELSE       ROLLBACK;   THROW; END CATCH; 

Oui, malheureusement, si lors d'une exécution de test, nous obtenons une transaction non validable, nous obtenons toujours une erreur dans l'exécution du scénario de test.

FakeTable et problème avec la clé étrangère


Considérez les tableaux familiers [Trial] et [Clinic]:


Nous nous souvenons du [TrialID] FK. Quels problèmes cela peut-il causer? Dans les exemples donnés précédemment, nous avons appliqué FakeTable aux deux tables à la fois. Si nous l'appliquons uniquement sur [Trial], nous obtenons la situation suivante:


Une tentative d'insertion d'une entrée dans [Clinic], de cette manière, peut s'avérer être un échec (même si nous avons préparé toutes les données nécessaires dans la fausse version du tableau [Trial]).

 [dbo].[Test_FK_Problem] failed: (Error) The INSERT statement conflicted with the FOREIGN KEY constraint "Trial_Fk". The conflict occurred in database "HabrDemo", table "dbo.tSQLt_tempobject_ba8f36353f7a44f6a9176a7d1db02493", column 'TrialID'.[16,0]{Test_FK_Problem,14} 

Conclusion: vous devez soit tout simuler, soit ne rien simuler. Dans le second cas, il est évident que la base doit être préparée à l'avance pour les tests.

SpyProcedure sur les procédures système


Hélas, l'espionnage des appels aux procédures système échouera.

 [HabrDemo].[test_test] failed: (Error) Cannot use SpyProcedure on sys.sp_help because the procedure does not exist[16,10] {tSQLt.Private_ValidateProcedureCanBeUsedWithSpyProcedure,7} 

Dans l'exemple du sémaphore, nous avons suivi les appels à la procédure [SendEmail] écrits par nos développeurs. Dans ce cas, l'écriture d'une procédure distincte est due à la nécessité de collecter et de traiter certaines informations supplémentaires avant d'envoyer des messages directement. Dans l'ensemble, il faut être mentalement préparé au fait que l'on peut avoir à écrire des procédures intercouches pour certaines procédures système uniquement à des fins de test.

Les avantages


Installation rapide


L'installation se déroule en 2 étapes et dure environ 2 minutes. Il vous suffit d'activer le CLR sur le serveur, si ce n'est déjà fait, et d'exécuter un seul script. Tout, vous pouvez ajouter la première classe de test et écrire des cas de test.

Développement rapide


tSQLt est un outil facile à apprendre. Il m'a fallu une petite journée pour le maîtriser. J'ai demandé à mes collègues qui travaillaient avec le cadre, et il s'est avéré que tout le monde passerait environ une journée.

Implémentation rapide dans CI


Il a fallu environ 2 heures pour configurer l'intégration dans CI sur notre projet. Bien sûr, le temps peut varier, mais en général ce n'est pas un problème et l'intégration peut être effectuée très rapidement.

Large gamme d'outils


Il s'agit d'une évaluation subjective, mais, à mon avis, la fonctionnalité fournie par tSQLt est assez riche et couvre la part du lion des besoins dans la pratique. Dans de rares cas où il n'y a pas assez de contrefaçons et d'assertions intégrées, il y a bien sûr tSQLt.Fail.

Documentation pratique


La documentation officielle est pratique et cohérente. Avec son aide, vous pouvez facilement comprendre l'essence de l'utilisation de tSQLt en peu de temps, même s'il s'agit de votre premier outil de test unitaire.

Sortie pratique


Les données peuvent être obtenues sous forme de texte très clair:

 [tSQLtDemo].[test_error_messages] failed: (Failure) Expected an error to be raised. [tSQLtDemo].[test_tables_comparison] failed: (Failure) useful and descriptive error message Unexpected/missing resultset rows! |_m_|Column1|Column2| +---+-------+-------+ |< |2 |Value2 | |= |1 |Value1 | |= |3 |Value3 | |> |2 |Value3 | +----------------------+ |Test Execution Summary| +----------------------+ |No|Test Case Name |Dur(ms)|Result | +--+------------------------------------+-------+-------+ |1 |[tSQLtDemo].[test_constraint] | 83|Success| |2 |[tSQLtDemo].[test_trial_view] | 83|Success| |3 |[tSQLtDemo].[test_error_messages] | 127|Failure| |4 |[tSQLtDemo].[test_tables_comparison]| 147|Failure| ----------------------------------------------------------------------------- Msg 50000, Level 16, State 10, Line 1 Test Case Summary: 4 test case(s) executed, 2 succeeded, 2 failed, 0 errored. ----------------------------------------------------------------------------- 

Vous pouvez également extraire de la base de données (cliquable) ...


... ou obtenez au format XML.

 <?xml version="1.0" encoding="UTF-8"?> <testsuites> <testsuite id="1" name="tSQLtDemo" tests="3" errors="0" failures="1" timestamp="2019-06-22T16:46:06" time="0.433" hostname="BLAHBLAHBLAH\SQL2017" package="tSQLt"> <properties /> <testcase classname="tSQLtDemo" name="test_constraint" time="0.097" /> <testcase classname="tSQLtDemo" name="test_error_messages" time="0.153"> <failure message="Expected an error to be raised." type="tSQLt.Fail" /> </testcase> <testcase classname="tSQLtDemo" name="test_trial_view" time="0.156" /> <system-out /> <system-err /> </testsuite> </testsuites> 

Cette dernière option vous permet d'intégrer facilement des tests dans CI. En particulier, tout fonctionne pour nous sous Atlassian Bamboo.

Prise en charge de Redgate


Les avantages incluent la prise en charge d'un si grand fournisseur d'outils DBA comme RedGate. SQL Test - leur plugin pour SQL Server Management Studio - fonctionne avec tSQLt dès la sortie de la boîte. En outre, RedGate fournit une assistance au développeur principal tSQLt avec l'environnement de développement, comme le prétend le développeur lui-même des groupes Google .

Inconvénients


Aucun faux table temporaire


tSQLt n'autorise pas les fausses tables temporaires. Si nécessaire, vous pouvez utiliser le module complémentaire non officiel . Malheureusement, ce module complémentaire n'est pris en charge que par SQL Server 2016+.

Pas d'accès aux bases de données externes


Il ne fonctionnera pas de conserver une base séparée uniquement pour stocker le cadre. tSQLt est conçu pour tester ce qui se trouve dans la même base de données. Le faux, hélas, ne fonctionnera pas.

 CREATE PROCEDURE [tSQLtDemo].[test_outer_db] AS BEGIN   SELECT TOP 10 * FROM [AdventureWorks2017].[Person].[Password]   EXEC tSQLt.FakeTable '[AdventureWorks2017].[Person].[Password]'   SELECT TOP 10 * FROM [AdventureWorks2017].[Person].[Password] END 


Les assertions semblent fonctionner, mais personne ne garantit leur performance, bien sûr.

 CREATE PROCEDURE [tSQLtDemo].[test_outer_db_assertions] AS BEGIN   SELECT TOP 1 *   INTO #Actual   FROM [AdventureWorks2017].[Person].[Password]   SELECT *   INTO #Expected   FROM (          SELECT 'bE3XiWw=' AS [PasswordSalt]   ) expectedresult;   EXEC tSQLt.AssertEqualsTable '#Expected', '#Actual', 'The salt is not salty'; END 


Bogues de documentation


Malgré le fait que j'ai écrit ci-dessus sur la cohérence et l'accessibilité de la documentation, elle contient également des problèmes. Il y a des moments dépassés.

Exemple 1. «Guide de démarrage rapide» suggère de télécharger le framework depuis SourceForge. Ils ont dit au revoir à SourceForge en 2015 .

Exemple 2. Le guide ApplyConstraint de l'exemple utilise une procédure Fail pour intercepter une exception, qui serait plus facile et plus visuelle à remplacer par ExpectException.

 CREATE PROCEDURE ConstraintTests.[test ReferencingTable_ReferencedTable_FK prevents insert of orphaned rows] AS BEGIN EXEC tSQLt.FakeTable 'dbo.ReferencedTable'; EXEC tSQLt.FakeTable 'dbo.ReferencingTable'; EXEC tSQLt.ApplyConstraint 'dbo.ReferencingTable','ReferencingTable_ReferencedTable_FK'; DECLARE @ErrorMessage NVARCHAR(MAX); SET @ErrorMessage = ''; /* []     ExceptException ? */ BEGIN TRY INSERT INTO dbo.ReferencingTable ( id, ReferencedTableId ) VALUES ( 1, 11 ) ; END TRY BEGIN CATCH SET @ErrorMessage = ERROR_MESSAGE(); END CATCH IF @ErrorMessage NOT LIKE '%ReferencingTable_ReferencedTable_FK%' BEGIN EXEC tSQLt.Fail 'Expected error message containing ''ReferencingTable_ReferencedTable_FK'' but got: ''',@ErrorMessage,'''!'; END END GO 

Et c'est naturel, car ça se passe ...

Abandon partiel


Il y a une longue interruption dans le développement de tSQLt de début 2016 à juin 2019. Oui, malheureusement, cet outil est partiellement abandonné. En 2019, peu à peu, à en juger par GitHub , le développement a encore progressé. Bien que les groupes Google officiels aient un fil dans lequel Sebastian, le principal développeur de tSQLt, a été directement interrogé sur le sort du développement. La dernière question a été posée le 2 mars 2019, la réponse n'a pas encore été reçue.

Problème avec SQL Server 2017


Si vous utilisez SQL Server 2017, alors pour vous, l'installation de tSQLt nécessitera éventuellement une manipulation supplémentaire. , 2012 SQL Server security-. «CLR strict security», ( SAFE). (, , ). .

, , « », tSQLt, , . GitHub issue , , 2017 (. ).

(±)


. tSQLt . , (CLR, T-SQL SQL ), , . , tSQLt , SQL-powered .

, PostgreSQL ptTAP . «» PL/pgSQL TAP. MySQL , — MyTAP . Oracle, utPLSQL- Un outil très puissant et activement (je dirais même plus que) un outil de développement.

Conclusion


, .

— . SQL Server, Oracle MySQL — . , . , , , , , .

— . , , SQL Server, tSQLt 100% , , . , , .

Source: https://habr.com/ru/post/fr461133/


All Articles