sql >> Database >  >> RDS >> Database

De kunst van het isoleren van afhankelijkheden en gegevens bij het testen van database-eenheden

Alle databaseontwikkelaars schrijven min of meer database-eenheidtests die niet alleen helpen bij het vroegtijdig detecteren van bugs, maar ook veel tijd en moeite besparen wanneer het onverwachte gedrag van databaseobjecten een productieprobleem wordt.

Tegenwoordig zijn er een aantal frameworks voor het testen van database-eenheden, zoals tSQLt, samen met tools voor het testen van eenheden van derden, waaronder dbForge Unit Test.

Aan de ene kant is het voordeel van het gebruik van testtools van derden dat het ontwikkelteam direct unit-tests met toegevoegde functies kan maken en uitvoeren. Bovendien geeft het direct gebruiken van een toetsingskader u meer controle over de unittests. Daarom kunt u meer functionaliteit toevoegen aan het unit-testraamwerk zelf. In dit geval moet uw team echter tijd en een bepaald niveau van expertise hebben om dit te doen.

Dit artikel onderzoekt enkele standaardpraktijken die ons kunnen helpen de manier waarop we database-eenheidtests schrijven te verbeteren.

Laten we eerst enkele sleutelconcepten van het testen van database-eenheden doornemen.

Wat is testen van database-eenheden

Volgens Dave Green zorgen database-eenheidstests ervoor dat kleine eenheden van de database, zoals tabellen, views, opgeslagen procedures, enz., werken zoals verwacht.

Database unit tests worden geschreven om te controleren of de code voldoet aan de zakelijke vereisten.

Als u bijvoorbeeld een eis krijgt zoals "Een bibliothecaris (eindgebruiker) moet nieuwe boeken aan de bibliotheek kunnen toevoegen (Management Informatiesysteem)", moet u overwegen unittests toe te passen voor de opgeslagen procedure om te controleren of het kan een nieuw boek toevoegen aan het Boek tafel.

Soms zorgt een reeks unit-tests ervoor dat de code aan de eisen voldoet. Daarom maken de meeste raamwerken voor unit-testen, waaronder tSQLt, het mogelijk om gerelateerde unit-tests in een enkele testklasse te groeperen in plaats van individuele tests uit te voeren.

AAA-principe

Het is het vermelden waard over het 3-stappenprincipe van unit-testing, dat een standaardpraktijk is om unit-tests te schrijven. Het AAA-principe is de basis voor unit testing en bestaat uit de volgende stappen:

  1. Rangschikken/assembleren
  2. Handelen
  3. Bevestigen

De Rangschik sectie is de eerste stap bij het schrijven van database-eenheidstests. Het begeleidt bij het configureren van een database-object voor het testen en het instellen van de verwachte resultaten.

De Wet sectie is wanneer een database-object (onder test) wordt aangeroepen om de daadwerkelijke uitvoer te produceren.

De Bevestigen stap gaat over het afstemmen van de werkelijke uitvoer op de verwachte uitvoer en controleert of de test slaagt of faalt.

Laten we deze methoden eens bekijken aan de hand van bepaalde voorbeelden.

Als we een eenheidstest maken om te verifiëren dat de AddProduct opgeslagen procedure kan een nieuw product toevoegen, we stellen het Product in en ExpectedProduct tabellen nadat het product is toegevoegd. In dit geval valt de methode onder de sectie Schikken/assembleren.

Het aanroepen van de AddProduct-procedure en het plaatsen van het resultaat in de Product-tabel valt onder de Act-sectie.

Het Assert-gedeelte matcht eenvoudig de Product-tabel met de ExpectedProduct-tabel om te zien of de opgeslagen procedure met succes of niet is uitgevoerd.

Afhankelijkheden in het testen van eenheden begrijpen

Tot nu toe hebben we de basisprincipes van het testen van database-eenheden en het belang van het AAA-principe (Assemble, Act en Assert) besproken bij het maken van een standaardeenheidstest.

Laten we ons nu concentreren op een ander belangrijk stukje van de puzzel:afhankelijkheden bij het testen van eenheden.

Afgezien van het volgen van het AAA-principe en ons alleen te concentreren op een bepaald database-object (onder test), moeten we ook de afhankelijkheden kennen die van invloed kunnen zijn op unit-tests.

De beste manier om afhankelijkheden te begrijpen, is door naar een voorbeeld van een eenheidstest te kijken.

EmployeesSample Database Setup

Maak om verder te gaan een voorbeelddatabase en noem deze EmployeesSample :

-- Create the Employees sample database to demonstrate unit testing

CREATE DATABASE EmployeesSample;
GO

Maak nu de Werknemer tabel in de voorbeelddatabase:

-- 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

Voorbeeldgegevens invullen

Vul de tabel door een paar records toe te voegen:

-- 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

De tabel ziet er als volgt uit:

-- View the Employee table

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

Houd er rekening mee dat ik in dit artikel dbForge Studio voor SQL Server gebruik. Het uiterlijk van de uitvoer kan dus verschillen als u dezelfde code uitvoert in SSMS (SQL Server Management Studio). Er is geen verschil als het gaat om scripts en hun resultaten.

Vereiste om nieuwe werknemer toe te voegen

Als er nu een vereiste is ontvangen om een ​​nieuwe werknemer toe te voegen, kunt u het beste aan de vereiste voldoen door een opgeslagen procedure te maken waarmee een nieuwe werknemer met succes aan de tabel kan worden toegevoegd.

Om dit te doen, maakt u de opgeslagen procedure AddEmployee als volgt aan:

-- 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

Eenheidstest om te controleren of aan de vereiste wordt voldaan

We gaan een database unit test schrijven om te verifiëren of de AddEmployee opgeslagen procedure voldoet aan de eis om een ​​nieuw record toe te voegen aan de Employee tabel.

Laten we ons concentreren op het begrijpen van de unit-testfilosofie door een unit-testcode te simuleren in plaats van een unit-test te schrijven met een testraamwerk of een unit-testtool van derden.

Eenheidstest simuleren en AAA-principe toepassen in SQL

Het eerste dat we moeten doen, is het AAA-principe in SQL imiteren, aangezien we geen raamwerk voor unit-testing gaan gebruiken.

De sectie Assembleren wordt toegepast wanneer de werkelijke en verwachte tabellen normaal gesproken worden ingesteld, samen met de verwachte tabel die wordt gevuld. We kunnen in deze stap gebruik maken van SQL-variabelen om de verwachte tabel te initialiseren.

De sectie Act wordt gebruikt wanneer de daadwerkelijke opgeslagen procedure wordt aangeroepen om gegevens in de daadwerkelijke tabel in te voegen.

De sectie Beweren is wanneer de verwachte tabel overeenkomt met de werkelijke tabel. Het simuleren van het Assert-gedeelte is een beetje lastig en kan worden bereikt door de volgende stappen:

  • Het tellen van de gemeenschappelijke (overeenkomende) rijen tussen twee tabellen die 1 zouden moeten zijn (aangezien de verwachte tabel slechts één record heeft die overeen moet komen met de werkelijke tabel)
  • Het uitsluiten van de werkelijke tabelrecords van de verwachte tabelrecords moet gelijk zijn aan 0 (als het record in de verwachte tabel ook in de werkelijke tabel voorkomt, zou het uitsluiten van alle werkelijke tabelrecords van de verwachte tabel 0 moeten opleveren)

Het SQL-script is als volgt:

[uitbreiden 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

[/uitbreiden]

Gesimuleerde eenheidstest uitvoeren

Nadat de opgeslagen procedure is gemaakt, voert u deze uit met de gesimuleerde eenheidstest:

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

De uitvoer is als volgt:

Gefeliciteerd! De test van de database-eenheid is geslaagd.

Identificatie van problemen in de vorm van afhankelijkheden in Unit Test

Kunnen we iets verkeerds detecteren in de eenheidstest die we hebben gemaakt, ondanks het feit dat deze is geschreven en met succes is uitgevoerd?

Als we goed kijken naar de unit testopstelling (het Assemble-gedeelte), heeft de verwachte tabel een onnodige binding met de identiteitskolom:

Alvorens een unittest te schrijven hebben we al 5 records toegevoegd aan de eigenlijke (Medewerkers)tabel. Bij de testopstelling begint de identiteitskolom voor de verwachte tabel dus met 6. Dit betekent echter dat we altijd verwachten dat er 5 records in de werkelijke (Werknemers)tabel staan ​​om deze overeen te laten komen met de verwachte tabel (#EmployeeExpected).

Laten we, om te begrijpen hoe dit de unit-test kan beïnvloeden, eens kijken naar de werkelijke (werknemers)tabel:

Voeg nog een record toe aan de tabel Werknemer:

-- Adding a new record to the Employee table

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

Bekijk nu de Werknemerstabel:

Verwijder EmpoyeeId 6 (Adil) zodat de unittest kan worden uitgevoerd tegen zijn eigen versie van EmployeeId 6 (Adil) in plaats van het eerder opgeslagen record.

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

DELETE FROM Employee
  WHERE EmployeeId=6

Voer de gesimuleerde eenheidstest uit en bekijk de resultaten:

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

EXEC TestAddEmployee

De test is deze keer mislukt. Het antwoord ligt in de resultatenset van de werknemerstabel zoals hieronder weergegeven:

De binding van de werknemer-ID in de eenheidstest, zoals hierboven vermeld, werkt niet wanneer we de eenheidstest opnieuw uitvoeren na het toevoegen van een nieuw record en het verwijderen van het eerder toegevoegde werknemersrecord.

Er zijn drie soorten afhankelijkheden in de test:

  1. Gegevensafhankelijkheid
  2. Belangrijke afhankelijkheid van beperkingen
  3. Afhankelijkheid identiteitskolom

Gegevensafhankelijkheid

Allereerst is deze eenheidstest afhankelijk van gegevens in de database. Volgens Dave Green zijn de gegevens zelf een afhankelijkheid als het gaat om de database voor het testen van eenheden.

Dit betekent dat uw database-eenheidstest niet mag vertrouwen op de gegevens in de database. Uw eenheidstest moet bijvoorbeeld de feitelijke gegevens bevatten die in het databaseobject (tabel) moeten worden ingevoegd in plaats van te vertrouwen op de gegevens die al in de database bestaan ​​en die kunnen worden verwijderd of gewijzigd.

In ons geval is het feit dat er al vijf records zijn ingevoegd in de eigenlijke werknemertabel een gegevensafhankelijkheid die moet worden voorkomen, omdat we de filosofie van de eenheidstest niet mogen schenden die zegt dat alleen de eenheid van de code wordt getest.

Met andere woorden, testgegevens mogen niet afhankelijk zijn van de feitelijke gegevens in de database.

Belangrijke beperking afhankelijkheid

Een andere afhankelijkheid is een afhankelijkheid van een sleutelbeperking, wat betekent dat de primaire sleutelkolom EmployeeId ook een afhankelijkheid is. Het moet voorkomen worden om een ​​goede unittest te kunnen schrijven. Er is echter een afzonderlijke eenheidstest vereist om een ​​primaire sleutelbeperking te testen.

Om bijvoorbeeld de opgeslagen procedure van AddEmployee te testen, moet de primaire sleutel van de tabel Werknemers worden verwijderd, zodat een object kan worden getest zonder dat u zich zorgen hoeft te maken dat een primaire sleutel wordt geschonden.

Identiteitskolomafhankelijkheid

Net als een primaire sleutelbeperking, is de identiteitskolom ook een afhankelijkheid. Het is dus niet nodig om de logica voor automatisch verhogen van de identiteitskolom te testen voor de AddEmployee-procedure; het moet koste wat kost worden vermeden.

Afhankelijkheden isoleren bij het testen van eenheden

We kunnen alle drie de afhankelijkheden voorkomen door de beperkingen tijdelijk uit de tabel te verwijderen en dan niet afhankelijk te zijn van de gegevens in de database voor de eenheidstest. Dit is hoe de standaard tests voor database-eenheden zijn geschreven.

In dit geval kan men zich afvragen waar de gegevens voor de tabel Werknemer vandaan komen. Het antwoord is dat de tabel wordt gevuld met testgegevens die zijn gedefinieerd in de eenheidstest.

Opgeslagen procedure voor eenheidstest wijzigen

Laten we nu de afhankelijkheden in onze eenheidstest verwijderen:

[uitbreiden 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

[/uitbreiden]

Afhankelijkheidsvrije gesimuleerde eenheidstest uitvoeren

Voer de gesimuleerde eenheidstest uit om de resultaten te zien:

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

EXEC TestAddEmployee

Voer de eenheidstest opnieuw uit om de opgeslagen procedure van AddEmployee te controleren:

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

EXEC TestAddEmployee

Gefeliciteerd! Afhankelijkheden van de eenheidstest zijn met succes verwijderd.

Zelfs als we nu een nieuw record of een nieuwe set records aan de tabel Werknemers toevoegen, heeft dit geen invloed op onze eenheidstest, aangezien we de gegevens en de beperkingsafhankelijkheden met succes uit de test hebben verwijderd.

Een database-eenheidstest maken met tSQLt

De volgende stap is het maken van een echte database-eenheidstest op basis van de gesimuleerde eenheidstest.

Als u SSMS (SQL Server Management Studio) gebruikt, moet u het tSQLt-framework installeren, een testklasse maken en CLR inschakelen voordat u de unit-test schrijft en uitvoert.

Als u dbForge Studio voor SQL Server gebruikt, kunt u de eenheidstest maken door met de rechtermuisknop op de opgeslagen procedure AddEmployee te klikken en vervolgens op "Unit Test" => "Add New Test..." te klikken, zoals hieronder weergegeven:

Om een ​​nieuwe test toe te voegen, vult u de vereiste informatie over de eenheidstest in:

Gebruik het volgende script om de eenheidstest te schrijven:

--  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

Voer vervolgens de database-eenheidtest uit:

Gefeliciteerd! We hebben met succes een database-eenheidstest gemaakt en uitgevoerd die vrij is van afhankelijkheden.

Dingen om te doen

Dat is het. U bent klaar om afhankelijkheden van database-eenheidstests te isoleren en database-eenheidstests te maken die vrij zijn van gegevens en beperkingsafhankelijkheden nadat u dit artikel hebt doorgenomen. Als resultaat kunt u uw vaardigheden verbeteren door de volgende dingen te doen:

  1. Probeer de opgeslagen procedure Werknemer verwijderen toe te voegen en maak een gesimuleerde database-eenheidstest voor Werknemer verwijderen met afhankelijkheden om te zien of het onder bepaalde omstandigheden mislukt
  2. Probeer de opgeslagen procedure voor het verwijderen van werknemer toe te voegen en maak een database-eenheidstest zonder afhankelijkheden om te zien of een werknemer kan worden verwijderd
  3. Probeer de opgeslagen procedure Werknemer zoeken toe te voegen en maak een gesimuleerde database-eenheidstest met afhankelijkheden om te zien of er naar een werknemer kan worden gezocht
  4. Probeer de opgeslagen procedure Werknemer zoeken toe te voegen en maak een database-eenheidstest zonder afhankelijkheden om te zien of er naar een werknemer kan worden gezocht
  5. Probeer complexere vereisten uit door opgeslagen procedures te maken om aan de vereisten te voldoen en vervolgens database-eenheidtests vrij van afhankelijkheden te schrijven om te zien of ze de test doorstaan ​​of niet. Zorg er echter voor dat de test herhaalbaar is en gericht is op het testen van de eenheid van de code

Handig hulpmiddel:

dbForge Unit Test – een intuïtieve en handige GUI voor het implementeren van geautomatiseerde unit-testing in SQL Server Management Studio.


  1. SQL Server-fout 113:eindcommentaar '*/' ontbreekt

  2. Hoe maak je een willekeurige string die geschikt is voor een sessie-ID in PostgreSQL?

  3. Mysql invoegen in 2 tabellen

  4. Hoe maak je een SQL Server-functie om meerdere rijen van een subquery samen te voegen in een enkel gescheiden veld?