sql >> Databasteknik >  >> RDS >> Database

Konsten att isolera beroenden och data i databasenhetstestning

Alla databasutvecklare skriver mer eller mindre databasenhetstester som inte bara hjälper till att upptäcka buggar tidigt utan också sparar mycket tid och ansträngningar när det oväntade beteendet hos databasobjekt blir ett produktionsproblem.

Nuförtiden finns det ett antal ramverk för testning av databasenheter som tSQLt tillsammans med verktyg för enhetstestning från tredje part inklusive dbForge Unit Test.

Å ena sidan är fördelen med att använda testverktyg från tredje part att utvecklingsteamet omedelbart kan skapa och köra enhetstester med extra funktioner. Att använda ett testramverk ger dig också direkt mer kontroll över enhetstesterna. Därför kan du lägga till mer funktionalitet till själva ramverket för enhetstestning. Men i det här fallet måste ditt team ha tid och en viss expertis för att göra detta.

Den här artikeln utforskar några standardmetoder som kan hjälpa oss att förbättra hur vi skriver databasenhetstester.

Låt oss först gå igenom några nyckelbegrepp för testning av databasenheter.

Vad är Databas Unit Testing

Enligt Dave Green säkerställer databasenhetstester att små enheter i databasen, såsom tabeller, vyer, lagrade procedurer, etc., fungerar som förväntat.

Databasenhetstester skrivs för att verifiera om koden uppfyller affärskraven.

Om du till exempel får ett krav som "En bibliotekarie (slutanvändare) ska kunna lägga till nya böcker till biblioteket (Management Information System)", måste du tänka på att tillämpa enhetstester för den lagrade proceduren för att kontrollera om den kan lägga till en ny bok i boken bord.

Ibland säkerställer en serie enhetstester att koden uppfyller kraven. Därför tillåter de flesta ramverk för enhetstestning, inklusive tSQLt, gruppering av relaterade enhetstester i en enda testklass snarare än att köra individuella tester.

AAA-princip

Det är värt att nämna om 3-stegsprincipen för enhetstestning som är en standardpraxis för att skriva enhetstester. AAA-principen är grunden för enhetstestning och består av följande steg:

  1. Arrangera/montera
  2. Göra
  3. Förstå

Arrangera avsnittet är det första steget i att skriva databasenhetstester. Den vägleder genom att konfigurera ett databasobjekt för att testa och ställa in de förväntade resultaten.

Akten sektionen är när ett databasobjekt (under test) anropas för att producera den faktiska utdata.

Förstå Steget handlar om att matcha den faktiska produktionen till den förväntade och verifiera om testet antingen godkänns eller inte.

Låt oss utforska dessa metoder med särskilda exempel.

Om vi ​​skapar ett enhetstest för att verifiera att AddProduct lagrad procedur kan lägga till en ny produkt, ställer vi in ​​Produkten och ExpectedProduct tabeller efter att produkten har lagts till. I det här fallet hamnar metoden under Arrange/Assemble-sektionen.

Att anropa AddProduct-proceduren och lägga in resultatet i produkttabellen täcks av lagen.

Assert-delen matchar helt enkelt Produkttabellen med ExpectedProduct-tabellen för att se om den lagrade proceduren har körts framgångsrikt eller misslyckats.

Förstå beroenden i enhetstestning

Hittills har vi diskuterat grunderna för databasenhetstestning och vikten av AAA-principen (Assemble, Act, and Assert) när du skapar ett standardenhetstest.

Låt oss nu fokusera på en annan viktig pusselbit – beroenden i enhetstestning.

Förutom att följa AAA-principen och bara fokusera på ett visst databasobjekt (under test), behöver vi också känna till de beroenden som kan påverka enhetstester.

Det bästa sättet att förstå beroenden är att titta på ett exempel på ett enhetstest.

Anställda Exempel Databas Setup

För att gå vidare, skapa en exempeldatabas och kalla den EmployeesSample :

-- Create the Employees sample database to demonstrate unit testing

CREATE DATABASE EmployeesSample;
GO

Skapa nu Anställd tabell i exempeldatabasen:

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

Pulera provdata

Fyll tabellen genom att lägga till några poster:

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

Tabellen ser ut så här:

-- View the Employee table

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

Observera att jag använder dbForge Studio för SQL Server i den här artikeln. Således kan utdatautseendet skilja sig om du kör samma kod i SSMS (SQL Server Management Studio). Det är ingen skillnad när det kommer till skript och deras resultat.

Krav för att lägga till ny anställd

Nu, om ett krav på att lägga till en ny anställd har mottagits, är det bästa sättet att uppfylla kravet att skapa en lagrad procedur som framgångsrikt kan lägga till en ny anställd i tabellen.

För att göra detta, skapa AddEmployee-lagrade proceduren enligt följande:

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

Enhetstest för att verifiera om kravet är uppfyllt

Vi kommer att skriva ett databasenhetstest för att verifiera om den lagrade proceduren AddEmployee uppfyller kravet att lägga till en ny post i Employee-tabellen.

Låt oss fokusera på att förstå enhetstestfilosofin genom att simulera en enhetstestkod snarare än att skriva ett enhetstest med ett testramverk eller ett enhetstestverktyg från tredje part.

Simulering av enhetstest och tillämpning av AAA-principen i SQL

Det första vi behöver göra är att imitera AAA-principen i SQL eftersom vi inte kommer att använda något ramverk för enhetstestning.

Monteringssektionen används när de faktiska och förväntade tabellerna normalt sätts upp tillsammans med den förväntade tabellen som fylls i. Vi kan använda SQL-variabler för att initiera den förväntade tabellen i detta steg.

Act-sektionen används när den faktiska lagrade proceduren anropas för att infoga data i själva tabellen.

Assert-sektionen är när den förväntade tabellen matchar den faktiska tabellen. Att simulera Assert-delen är lite knepigt och kan uppnås genom följande steg:

  • Räknar de vanliga (matchande) raderna mellan två tabeller som ska vara 1 (eftersom den förväntade tabellen bara har en post som ska matcha den faktiska tabellen)
  • Att exkludera de faktiska tabellposterna från de förväntade tabellposterna bör vara lika med 0 (om posten i den förväntade tabellen också finns i den faktiska tabellen, då bör exkludering av alla faktiska tabellposter från den förväntade tabellen returnera 0)

SQL-skriptet är som följer:

[expand title="Kod"]

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

Kör simulerat enhetstest

Efter att den lagrade proceduren har skapats, kör den med det simulerade enhetstestet:

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

Utgången är som följer:

Grattis! Databasenhetstestet godkändes.

Identifiera problem i form av beroenden i enhetstest

Kan vi upptäcka något fel i enhetstestet vi skapade trots att det har skrivits och körts framgångsrikt?

Om vi ​​tittar noga på enhetens testsetup (monteringsdelen), har den förväntade tabellen en onödig bindning med identitetskolumnen:

Innan vi skriver ett enhetstest har vi redan lagt till 5 poster i den faktiska (anställda) tabellen. Sålunda, vid testinställningen, börjar identitetskolumnen för den förväntade tabellen med 6. Detta betyder dock att vi alltid förväntar oss att 5 poster finns i den faktiska (Anställd) tabellen för att matcha den med den förväntade tabellen (#EmployeeExpected).

För att förstå hur detta kan påverka enhetstestet, låt oss ta en titt på den faktiska (anställda) tabellen nu:

Lägg till ytterligare en post i tabellen Employee:

-- Adding a new record to the Employee table

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

Ta en titt på tabellen för anställda nu:

Ta bort EmployeeId 6 (Adil) så att enhetstestet kan köras mot sin egen version av EmployeeId 6 (Adil) snarare än den tidigare lagrade posten.

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

DELETE FROM Employee
  WHERE EmployeeId=6

Kör det simulerade enhetstestet och se resultaten:

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

EXEC TestAddEmployee

Testet har misslyckats den här gången. Svaret finns i resultatuppsättningen för personaltabellen enligt nedan:

Medarbetar-ID-bindningen i enhetstestet som nämnts ovan fungerar inte när vi kör enhetstestet igen efter att ha lagt till en ny post och raderat den tidigare tillagda personalposten.

Det finns tre typer av beroenden i testet:

  1. Databeroende
  2. Nyckelbegränsningsberoende
  3. Identitetskolumnberoende

Databeroende

Först och främst beror detta enhetstest på data i databasen. Enligt Dave Green, när det kommer till enhetstestdatabasen, är själva data ett beroende.

Detta innebär att ditt databasenhetstest inte bör förlita sig på data i databasen. Till exempel bör ditt enhetstest innehålla de faktiska data som ska infogas i databasobjektet (tabellen) istället för att förlita sig på de data som redan finns i databasen som kan raderas eller ändras.

I vårt fall är det faktum att fem poster redan har infogats i den faktiska Employee-tabellen ett databeroende som måste förhindras eftersom vi inte bör bryta mot filosofin med enhetstest som säger att bara enheten i koden testas.

Med andra ord bör testdata inte förlita sig på faktiska data i databasen.

Nyckelbegränsningsberoende

Ett annat beroende är ett nyckelbegränsningsberoende vilket betyder att den primära nyckelkolumnen EmployeeId också är ett beroende. Det måste förhindras för att kunna skriva ett bra enhetstest. Ett separat enhetstest krävs dock för att testa en primärnyckelbegränsning.

Till exempel, för att testa AddEmployee-lagrade proceduren, bör Employee-tabellens primärnyckel tas bort så att ett objekt kan testas utan att behöva oroa sig för att bryta mot en primärnyckel.

Identitetskolumnberoende

Precis som en primärnyckelbegränsning är identitetskolumnen också ett beroende. Det finns alltså inget behov av att testa logiken för automatisk ökning av identitetskolumnen för AddEmployee-proceduren; det måste undvikas till varje pris.

Isolera beroenden i enhetstestning

Vi kan förhindra alla tre beroenden genom att tillfälligt ta bort begränsningarna från tabellen och sedan inte vara beroende av data i databasen för enhetstestet. Så här skrivs standardtesterna för databasenhet.

I det här fallet kan man fråga sig varifrån uppgifterna för tabellen Employee kom. Svaret är att tabellen fylls i med testdata som definierats i enhetstestet.

Ändra enhetstest lagrad procedur

Låt oss nu ta bort beroenden i vårt enhetstest:

[expand title="Kod"]

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

Kör beroendefritt simulerat enhetstest

Kör det simulerade enhetstestet för att se resultaten:

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

EXEC TestAddEmployee

Kör enhetstestet igen för att kontrollera AddEmployee-lagrade proceduren:

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

EXEC TestAddEmployee

Grattis! Beroenden från enhetstestet har tagits bort.

Nu, även om vi lägger till en ny post eller uppsättning nya poster i tabellen Employee, kommer det inte att påverka vårt enhetstest eftersom vi har tagit bort data och begränsningsberoenden från testet.

Skapa databasenhetstest med tSQLt

Nästa steg är att skapa ett riktigt databasenhetstest baserat på det simulerade enhetstestet.

Om du använder SSMS (SQL Server Management Studio) måste du installera tSQLt-ramverket, skapa en testklass och aktivera CLR innan du skriver och kör enhetstestet.

Om du använder dbForge Studio för SQL Server kan du skapa enhetstestet genom att högerklicka på den lagrade proceduren AddEmployee och sedan klicka på "Unit Test" => "Add New Test..." som visas nedan:

För att lägga till ett nytt test, fyll i nödvändig enhetstestinformation:

För att skriva enhetstestet, använd följande skript:

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

Kör sedan databasenhetstestet:

Grattis! Vi har framgångsrikt skapat och kört databasenhetstest som är fritt från beroenden.

Saker att göra

Det är allt. Du är redo att isolera beroenden från databasenhetstester och skapa databasenhetstest fria från data och begränsningsberoenden efter att ha gått igenom den här artikeln. Som ett resultat kan du förbättra dina färdigheter genom att utföra följande saker:

  1. Försök att lägga till den lagrade proceduren för Ta bort anställd och skapa ett simulerat databasenhetstest för Ta bort anställd med beroenden för att se om det misslyckas under vissa förhållanden
  2. Försök att lägga till den lagrade proceduren Ta bort anställd och skapa ett databasenhetstest fritt från beroenden för att se om en anställd kan tas bort
  3. Försök att lägga till Search Employee-lagrade proceduren och skapa ett simulerat databasenhetstest med beroenden för att se om en anställd kan sökas efter
  4. Försök att lägga till Search Employee-lagrade proceduren och skapa ett databasenhetstest fritt från beroenden för att se om en anställd kan sökas efter
  5. Försök mer komplexa krav genom att skapa lagrade procedurer för att uppfylla kraven och sedan skriva databasenhetstester fria från beroenden för att se om de klarar testet eller misslyckas. Se dock till att testet är repeterbart och fokuserat på att testa enheten för koden

Användbart verktyg:

dbForge Unit Test – ett intuitivt och bekvämt gränssnitt för att implementera automatiserad enhetstestning i SQL Server Management Studio.


  1. Flera INSERT-satser kontra enkla INSERT med flera VÄRDEN

  2. mysql - hur många kolumner är för många?

  3. Frågan tar timeout från webbappen men körs bra från managementstudio

  4. Topptrender inom databashantering