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:
- Rangschikken/assembleren
- Handelen
- 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:
- Gegevensafhankelijkheid
- Belangrijke afhankelijkheid van beperkingen
- 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:
- 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
- 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
- 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
- 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
- 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.