Database
 sql >> Base de données >  >> RDS >> Database

L'art d'isoler les dépendances et les données dans les tests unitaires de base de données

Tous les développeurs de bases de données écrivent plus ou moins des tests unitaires de base de données qui aident non seulement à détecter les bogues tôt, mais aussi à économiser beaucoup de temps et d'efforts lorsque le comportement inattendu des objets de la base de données devient un problème de production.

De nos jours, il existe un certain nombre de cadres de test unitaire de base de données tels que tSQLt ainsi que des outils de test unitaire tiers, notamment dbForge Unit Test.

D'une part, l'avantage d'utiliser des outils de test tiers est que l'équipe de développement peut instantanément créer et exécuter des tests unitaires avec des fonctionnalités supplémentaires. De plus, l'utilisation directe d'un framework de test vous donne plus de contrôle sur les tests unitaires. Par conséquent, vous pouvez ajouter plus de fonctionnalités au framework de test unitaire lui-même. Cependant, dans ce cas, votre équipe doit avoir le temps et un certain niveau d'expertise pour le faire.

Cet article explore certaines pratiques standard qui peuvent nous aider à améliorer la façon dont nous écrivons les tests unitaires de base de données.

Tout d'abord, passons en revue certains concepts clés des tests unitaires de base de données.

Qu'est-ce que les tests unitaires de base de données ?

Selon Dave Green, les tests unitaires de base de données garantissent que les petites unités de la base de données, telles que les tables, les vues, les procédures stockées, etc., fonctionnent comme prévu.

Les tests unitaires de base de données sont écrits pour vérifier si le code répond aux exigences de l'entreprise.

Par exemple, si vous recevez une exigence telle que "Un bibliothécaire (utilisateur final) doit pouvoir ajouter de nouveaux livres à la bibliothèque (Système d'information de gestion)", vous devez penser à appliquer des tests unitaires pour la procédure stockée afin de vérifier si il peut ajouter un nouveau livre au Livre tableau.

Parfois, une série de tests unitaires garantit que le code répond aux exigences. Par conséquent, la plupart des frameworks de tests unitaires, y compris tSQLt, permettent de regrouper les tests unitaires associés dans une seule classe de test plutôt que d'exécuter des tests individuels.

Principe AAA

Il convient de mentionner le principe en 3 étapes des tests unitaires qui est une pratique standard pour écrire des tests unitaires. Le principe AAA est la base des tests unitaires et se compose des étapes suivantes :

  1. Arranger/Assembler
  2. Agir
  3. Assister

Le Arrangement La section est la première étape dans l'écriture des tests unitaires de base de données. Il vous guide tout au long de la configuration d'un objet de base de données pour tester et configurer les résultats attendus.

La loi section est lorsqu'un objet de base de données (en cours de test) est appelé pour produire la sortie réelle.

L'assertion L'étape consiste à faire correspondre la sortie réelle à celle attendue et vérifie si le test réussit ou échoue.

Explorons ces méthodes sur des exemples particuliers.

Si nous créons un test unitaire pour vérifier que le AddProduct procédure stockée peut ajouter un nouveau produit, nous configurons le Produit et Produit Attendu tables après l'ajout du produit. Dans ce cas, la méthode se trouve dans la section Arranger/Assembler.

L'appel de la procédure AddProduct et le placement du résultat dans la table Product sont couverts par la section Act.

La partie Assert fait simplement correspondre la table Product avec la table ExpectedProduct pour voir si la procédure stockée a été exécutée avec succès ou a échoué.

Comprendre les dépendances dans les tests unitaires

Jusqu'à présent, nous avons discuté des bases des tests unitaires de base de données et de l'importance du principe AAA (Assemble, Act, and Assert) lors de la création d'un test unitaire standard.

Maintenant, concentrons-nous sur une autre pièce importante du puzzle :les dépendances dans les tests unitaires.

En plus de suivre le principe AAA et de se concentrer uniquement sur un objet de base de données particulier (en cours de test), nous devons également connaître les dépendances qui peuvent affecter les tests unitaires.

La meilleure façon de comprendre les dépendances est de regarder un exemple de test unitaire.

Configuration de la base de données d'échantillons d'employés

Pour continuer, créez un exemple de base de données et appelez-le EmployeesSample :

-- Create the Employees sample database to demonstrate unit testing

CREATE DATABASE EmployeesSample;
GO

Maintenant, créez l'Employé table dans la base de données exemple :

-- Create the Employee table in the sample database

USE EmployeesSample

CREATE TABLE Employee
  (EmployeeId INT PRIMARY KEY IDENTITY(1,1),
  NAME VARCHAR(40),
  StartDate DATETIME2,
  Title VARCHAR(50)
  );
GO

Remplir des exemples de données

Remplissez le tableau en ajoutant quelques enregistrements :

-- Adding data to the Employee table
INSERT INTO Employee (NAME, StartDate, Title)
  VALUES 
  ('Sam','2018-01-01', 'Developer'),
  ('Asif','2017-12-12','Tester'),
  ('Andy','2016-10-01','Senior Developer'),
  ('Peter','2017-11-01','Infrastructure Engineer'),
  ('Sadaf','2015-01-01','Business Analyst');
GO

Le tableau ressemble à ceci :

-- View the Employee table

  SELECT e.EmployeeId
        ,e.NAME
        ,e.StartDate
        ,e.Title FROM  Employee e;
GO

Veuillez noter que j'utilise dbForge Studio pour SQL Server dans cet article. Ainsi, l'aspect de la sortie peut différer si vous exécutez le même code dans SSMS (SQL Server Management Studio). Il n'y a aucune différence en ce qui concerne les scripts et leurs résultats.

Exigence pour ajouter un nouvel employé

Maintenant, si une exigence d'ajouter un nouvel employé a été reçue, la meilleure façon de répondre à l'exigence est de créer une procédure stockée qui peut ajouter avec succès un nouvel employé à la table.

Pour cela, créez la procédure stockée AddEmployee comme suit :

-- Stored procedure to add a new employee 

CREATE PROCEDURE AddEmployee @Name VARCHAR(40),
@StartDate DATETIME2,
@Title VARCHAR(50)
AS
BEGIN
  SET NOCOUNT ON
    INSERT INTO Employee (NAME, StartDate, Title)
  VALUES (@Name, @StartDate, @Title);
END

Test unitaire pour vérifier si l'exigence est satisfaite

Nous allons écrire un test unitaire de base de données pour vérifier si la procédure stockée AddEmployee répond à l'exigence d'ajouter un nouvel enregistrement à la table Employee.

Concentrons-nous sur la compréhension de la philosophie des tests unitaires en simulant un code de test unitaire plutôt qu'en écrivant un test unitaire avec un framework de test ou un outil de test unitaire tiers.

Simulation des tests unitaires et application du principe AAA en SQL

La première chose que nous devons faire est d'imiter le principe AAA en SQL puisque nous n'allons pas utiliser de framework de test unitaire.

La section Assembler est appliquée lorsque les tables réelles et attendues sont normalement configurées et que la table attendue est remplie. Nous pouvons utiliser des variables SQL pour initialiser la table attendue à cette étape.

La section Act est utilisée lorsque la procédure stockée réelle est appelée pour insérer des données dans la table réelle.

La section Assert correspond au moment où la table attendue correspond à la table réelle. La simulation de la partie Assert est un peu délicate et peut être réalisée en procédant comme suit :

  • Compter les lignes communes (correspondantes) entre deux tables qui devraient être 1 (puisque la table attendue n'a qu'un seul enregistrement qui doit correspondre à la table réelle)
  • L'exclusion des enregistrements de table réels des enregistrements de table attendus doit être égale à 0 (si l'enregistrement de la table attendue existe également dans la table réelle, l'exclusion de tous les enregistrements de table réels de la table attendue doit renvoyer 0)

Le script SQL est le suivant :

[expand title=”Code”]

-- Simulating unit test to test the AddEmployee stored procedure

CREATE PROCEDURE TestAddEmployee
AS
BEGIN
  -- (1) Assemble

  -- Set up new employee data
  DECLARE @EmployeeId INT = 6
         ,@NAME VARCHAR(40) = 'Adil'
         ,@StartDate DATETIME2 = '2018-03-01'
         ,@Title VARCHAR(50) = 'Development Manager'


  -- Set up the expected table
  CREATE TABLE #EmployeeExpected (
    EmployeeId INT PRIMARY KEY IDENTITY (6, 1) 
    -- the expected table EmployeeId should begin with 6 
    -- since the actual table has already got 5 records and 
    -- the next EmployeeId in the actual table is 6
   ,NAME VARCHAR(40)
   ,StartDate DATETIME2
   ,Title VARCHAR(50)
  );

  -- Add the expected table data
  INSERT INTO #EmployeeExpected (NAME, StartDate, Title)
    VALUES (@NAME, @StartDate, @Title);

  -- (2) Act

  -- Call AddEmployee to add new employee data to the Employee table
  INSERT INTO Employee
  EXEC AddEmployee @NAME
                  ,@StartDate
                  ,@Title



  -- (3) Assert

  -- Match the actual table with the expected table
  DECLARE @ActualAndExpectedTableCommonRecords INT = 0 -- we assume that expected and actual table records have nothing in common

  SET @ActualAndExpectedTableCommonRecords = (SELECT
      COUNT(*)
    FROM (SELECT
        e.EmployeeId
       ,e.NAME
       ,e.StartDate
       ,e.Title
      FROM Employee e
      INTERSECT
      SELECT
        ee.EmployeeId
       ,ee.NAME
       ,ee.StartDate
       ,ee.Title
      FROM #EmployeeExpected ee) AS A)


  DECLARE @ExpectedTableExcluldingActualTable INT = 1 -- we assume that expected table has records which do not exist in the actual table

  SET @ExpectedTableExcluldingActualTable = (SELECT
      COUNT(*)
    FROM (SELECT
        ee.EmployeeId
       ,ee.NAME
       ,ee.StartDate
       ,ee.Title
      FROM #EmployeeExpected ee
      EXCEPT
      SELECT
        e.EmployeeId
       ,e.NAME
       ,e.StartDate
       ,e.Title
      FROM Employee e) AS A)


  IF @ActualAndExpectedTableCommonRecords = 1
    AND @ExpectedTableExcluldingActualTable = 0
    PRINT '*** Test Passed! ***'
  ELSE
    PRINT '*** Test Failed! ***'

END

[/expand]

Exécuter un test unitaire simulé

Une fois la procédure stockée créée, exécutez-la avec le test unitaire simulé :

-- Running simulated unit test to check the AddEmployee stored procedure
EXEC TestAddEmployee

Le résultat est le suivant :

Toutes nos félicitations! Le test unitaire de la base de données a réussi.

Identifier les problèmes sous la forme de dépendances dans les tests unitaires

Pouvons-nous détecter quelque chose d'anormal dans le test unitaire que nous avons créé malgré le fait qu'il ait été écrit et exécuté avec succès ?

Si nous examinons de près la configuration du test unitaire (la partie Assembler), la table attendue a une liaison inutile avec la colonne d'identité :

Avant d'écrire un test unitaire, nous avons déjà ajouté 5 enregistrements à la table réelle (Employé). Ainsi, lors de la configuration du test, la colonne d'identité de la table attendue commence par 6. Cependant, cela signifie que nous nous attendons toujours à ce que 5 enregistrements se trouvent dans la table réelle (Employee) pour la faire correspondre avec la table attendue (#EmployeeExpected).

Afin de comprendre comment cela peut affecter le test unitaire, examinons maintenant le tableau réel (Employé) :

Ajoutez un autre enregistrement à la table Employé :

-- Adding a new record to the Employee table

INSERT INTO Employee (NAME, StartDate, Title)
  VALUES ('Mark', '2018-02-01', 'Developer');

Jetez un œil à la table des employés maintenant :

Supprimez l'EmpoyeeId 6 (Adil) afin que le test unitaire puisse s'exécuter sur sa propre version d'EmployeeId 6 (Adil) plutôt que sur l'enregistrement précédemment stocké.

-- Deleting the previously created EmployeeId: 6 (Adil) record from the Employee table

DELETE FROM Employee
  WHERE EmployeeId=6

Exécutez le test unitaire simulé et consultez les résultats :

-- Running the simulated unit test to check the AddEmployee stored procedure

EXEC TestAddEmployee

Le test a échoué cette fois. La réponse se trouve dans l'ensemble de résultats de la table Employee, comme indiqué ci-dessous :

La liaison de l'identifiant de l'employé dans le test unitaire, comme mentionné ci-dessus, ne fonctionne pas lorsque nous réexécutons le test unitaire après avoir ajouté un nouvel enregistrement et supprimé l'enregistrement de l'employé précédemment ajouté.

Il existe trois types de dépendances dans le test :

  1. Dépendance des données
  2. Dépendance de la contrainte clé
  3. Dépendance de la colonne d'identité

Dépendance des données

Tout d'abord, ce test unitaire dépend des données de la base de données. Selon Dave Green, lorsqu'il s'agit de la base de données de tests unitaires, les données elles-mêmes sont une dépendance.

Cela signifie que votre test unitaire de base de données ne doit pas s'appuyer sur les données de la base de données. Par exemple, votre test unitaire doit contenir les données réelles à insérer dans l'objet de base de données (table) plutôt que de s'appuyer sur les données déjà existantes dans la base de données qui peuvent être supprimées ou modifiées.

Dans notre cas, le fait que cinq enregistrements aient déjà été insérés dans la table Employee réelle est une dépendance de données qui doit être évitée car nous ne devons pas violer la philosophie du test unitaire qui dit que seule l'unité du code est testée.

En d'autres termes, les données de test ne doivent pas reposer sur les données réelles de la base de données.

Dépendance de la contrainte clé

Une autre dépendance est une dépendance de contrainte de clé, ce qui signifie que la colonne de clé primaire EmployeeId est également une dépendance. Il faut le prévenir pour écrire un bon test unitaire. Cependant, un test unitaire séparé est requis pour tester une contrainte de clé primaire.

Par exemple, pour tester la procédure stockée AddEmployee, la clé primaire de la table Employee doit être supprimée afin qu'un objet puisse être testé sans craindre de violer une clé primaire.

Dépendance de la colonne d'identité

Tout comme une contrainte de clé primaire, la colonne d'identité est également une dépendance. Ainsi, il n'est pas nécessaire de tester la logique d'auto-incrémentation de la colonne d'identité pour la procédure AddEmployee; il faut l'éviter à tout prix.

Isolation des dépendances dans les tests unitaires

Nous pouvons empêcher les trois dépendances en supprimant temporairement les contraintes de la table, puis en ne dépendant pas des données de la base de données pour le test unitaire. C'est ainsi que les tests unitaires de base de données standard sont écrits.

Dans ce cas, on peut se demander d'où proviennent les données de la table Employee. La réponse est que le tableau est rempli avec les données de test définies dans le test unitaire.

Modification de la procédure stockée de test unitaire

Supprimons maintenant les dépendances dans notre test unitaire :

[expand title=”Code”]

-- Simulating dependency free unit test to test the AddEmployee stored procedure
ALTER PROCEDURE TestAddEmployee
AS
BEGIN
  -- (1) Assemble

  -- Set up new employee data
  DECLARE @NAME VARCHAR(40) = 'Adil'
         ,@StartDate DATETIME2 = '2018-03-01'
         ,@Title VARCHAR(50) = 'Development Manager'

  -- Set actual table
  DROP TABLE Employee -- drop table to remove dependencies

  CREATE TABLE Employee -- create a table without dependencies (PRIMARY KEY and IDENTITY(1,1))
  (
    EmployeeId INT DEFAULT(0)
   ,NAME VARCHAR(40)
   ,StartDate DATETIME2
   ,Title VARCHAR(50)
  )

  -- Set up the expected table without dependencies (PRIMARY KEY and IDENTITY(1,1)
  CREATE TABLE #EmployeeExpected (
    EmployeeId INT DEFAULT(0)
   ,NAME VARCHAR(40)
   ,StartDate DATETIME2
   ,Title VARCHAR(50)
  )

  -- Add the expected table data
  INSERT INTO #EmployeeExpected (NAME, StartDate, Title)
    VALUES (@NAME, @StartDate, @Title)

  -- (2) Act

  -- Call AddEmployee to add new employee data to the Employee table
  EXEC AddEmployee @NAME
                  ,@StartDate
                  ,@Title
 
  -- (3) Assert

  -- Match the actual table with the expected table
  DECLARE @ActualAndExpectedTableCommonRecords INT = 0 -- we assume that the expected and actual table records have nothing in common

  SET @ActualAndExpectedTableCommonRecords = (SELECT
      COUNT(*)
    FROM (SELECT
        e.EmployeeId
       ,e.NAME
       ,e.StartDate
       ,e.Title
      FROM Employee e
      INTERSECT
      SELECT
        ee.EmployeeId
       ,ee.NAME
       ,ee.StartDate
       ,ee.Title
      FROM #EmployeeExpected ee) AS A)


  DECLARE @ExpectedTableExcluldingActualTable INT = 1 -- we assume that the expected table has records which donot exist in actual table

  SET @ExpectedTableExcluldingActualTable = (SELECT
      COUNT(*)
    FROM (SELECT
        ee.EmployeeId
       ,ee.NAME
       ,ee.StartDate
       ,ee.Title
      FROM #EmployeeExpected ee
      EXCEPT
      SELECT
        e.EmployeeId
       ,e.NAME
       ,e.StartDate
       ,e.Title
      FROM Employee e) AS A)


  IF @ActualAndExpectedTableCommonRecords = 1
    AND @ExpectedTableExcluldingActualTable = 0
    PRINT '*** Test Passed! ***'
  ELSE
    PRINT '*** Test Failed! ***'

  -- View the actual and expected tables before comparison
    SELECT e.EmployeeId
          ,e.NAME
          ,e.StartDate
          ,e.Title FROM Employee e

      SELECT    ee.EmployeeId
               ,ee.NAME
               ,ee.StartDate
               ,ee.Title FROM #EmployeeExpected ee
  
  -- Reset the table (Put back constraints after the unit test)
  DROP TABLE Employee
  DROP TABLE #EmployeeExpected

  CREATE TABLE Employee (
    EmployeeId INT PRIMARY KEY IDENTITY (1, 1)
   ,NAME VARCHAR(40)
   ,StartDate DATETIME2
   ,Title VARCHAR(50)
  );

END

[/expand]

Exécution d'un test unitaire simulé sans dépendance

Exécutez le test unitaire simulé pour voir les résultats :

-- Running the dependency-free simulated unit test to check the AddEmployee stored procedure

EXEC TestAddEmployee

Relancez le test unitaire pour vérifier la procédure stockée AddEmployee :

-- Running the dependency-free simulated unit test to check the AddEmployee stored procedure

EXEC TestAddEmployee

Toutes nos félicitations! Les dépendances du test unitaire ont été supprimées avec succès.

Maintenant, même si nous ajoutons un nouvel enregistrement ou un ensemble de nouveaux enregistrements à la table Employee, cela n'affectera pas notre test unitaire puisque nous avons supprimé les dépendances de données et de contraintes du test avec succès.

Création d'un test unitaire de base de données à l'aide de tSQLt

L'étape suivante consiste à créer un test unitaire de base de données réel basé sur le test unitaire simulé.

Si vous utilisez SSMS (SQL Server Management Studio), vous devrez installer le framework tSQLt, créer une classe de test et activer CLR avant d'écrire et d'exécuter le test unitaire.

Si vous utilisez dbForge Studio pour SQL Server, vous pouvez créer le test unitaire en cliquant avec le bouton droit sur la procédure stockée AddEmployee, puis en cliquant sur "Test unitaire" => "Ajouter un nouveau test…" comme indiqué ci-dessous :

Pour ajouter un nouveau test, remplissez les informations de test unitaire requises :

Pour écrire le test unitaire, utilisez le script suivant :

--  Comments here are associated with the test.
--  For test case examples, see: http://tsqlt.org/user-guide/tsqlt-tutorial/
CREATE PROCEDURE [BasicTests].[test if new employee can be added]
AS
BEGIN
  --Assemble
  DECLARE @NAME VARCHAR(40) = 'Adil'
         ,@StartDate DATETIME2 = '2018-03-01'
         ,@Title VARCHAR(50) = 'Development Manager'


  EXEC tSQLt.FakeTable "dbo.Employee" -- This will create a dependency-free copy of the Employee table
  
  CREATE TABLE BasicTests.Expected -- Create the expected table
  (
    EmployeeId INT 
    ,NAME VARCHAR(40)
   ,StartDate DATETIME2
   ,Title VARCHAR(50)
  )


  -- Add the expected table data
  INSERT INTO BasicTests.Expected (NAME, StartDate, Title)
    VALUES (@NAME, @StartDate, @Title)

  --Act
  EXEC AddEmployee @Name -- Insert data into the Employee table
                  ,@StartDate 
                  ,@Title 
  

  --Assert 
  EXEC tSQLt.AssertEqualsTable @Expected = N'BasicTests.Expected'
                              ,@Actual = N'dbo.Employee'
                              ,@Message = N'Actual table matched with expected table'
                              ,@FailMsg = N'Actual table does not match with expected table'

END;
GO

Ensuite, lancez le test unitaire de la base de données :

Toutes nos félicitations! Nous avons créé et exécuté avec succès un test unitaire de base de données qui est exempt de dépendances.

Choses à faire

C'est ça. Vous êtes prêt à isoler les dépendances des tests unitaires de base de données et à créer un test unitaire de base de données sans dépendances de données et de contraintes après avoir parcouru cet article. En conséquence, vous pouvez améliorer vos compétences en effectuant les tâches suivantes :

  1. Veuillez essayer d'ajouter la procédure stockée Supprimer l'employé et créer un test unitaire de base de données simulé pour Supprimer l'employé avec des dépendances pour voir s'il échoue dans certaines conditions
  2. Veuillez essayer d'ajouter la procédure stockée Supprimer l'employé et créer un test unitaire de base de données sans dépendances pour voir si un employé peut être supprimé
  3. Veuillez essayer d'ajouter la procédure stockée Rechercher un employé et créer un test unitaire de base de données simulé avec des dépendances pour voir si un employé peut être recherché
  4. Veuillez essayer d'ajouter la procédure stockée Rechercher un employé et créer un test unitaire de base de données sans dépendances pour voir si un employé peut être recherché
  5. Veuillez essayer des exigences plus complexes en créant des procédures stockées pour répondre aux exigences, puis en écrivant des tests unitaires de base de données sans dépendances pour voir si elles réussissent le test ou échouent. Cependant, assurez-vous que le test est reproductible et axé sur le test de l'unité du code

Outil utile :

dbForge Unit Test - une interface graphique intuitive et pratique pour la mise en œuvre de tests unitaires automatisés dans SQL Server Management Studio.