Alle databaseudviklere skriver mere eller mindre databaseenhedstest, der ikke kun hjælper med at opdage fejl tidligt, men også sparer en masse tid og kræfter, når databaseobjekternes uventede adfærd bliver et produktionsproblem.
I dag er der en række databaseenhedstestrammer såsom tSQLt sammen med tredjeparts enhedstestværktøjer, herunder dbForge Unit Test.
På den ene side er fordelen ved at bruge tredjeparts testværktøjer, at udviklingsteamet øjeblikkeligt kan oprette og køre enhedstests med tilføjede funktioner. Brug af en testramme giver dig også direkte mere kontrol over enhedstestene. Derfor kan du tilføje mere funktionalitet til selve enhedstestrammerne. Men i dette tilfælde skal dit team have tid og et vist niveau af ekspertise til at gøre dette.
Denne artikel udforsker nogle standardpraksis, der kan hjælpe os med at forbedre den måde, vi skriver databaseenhedstest på.
Lad os først gennemgå nogle nøglebegreber for test af databaseenheder.
Hvad er Database Unit Testing
Ifølge Dave Green sikrer databaseenhedstest, at små enheder i databasen, såsom tabeller, visninger, lagrede procedurer osv., fungerer som forventet.
Databaseenhedstests er skrevet for at verificere, om koden opfylder forretningskrav.
Hvis du f.eks. modtager et krav som "En bibliotekar (slutbruger) skal være i stand til at tilføje nye bøger til biblioteket (Management Information System)", skal du tænke på at anvende enhedstest til den lagrede procedure for at kontrollere, om den kan tilføje en ny bog til bogen tabel.
Nogle gange sikrer en række enhedstests, at koden opfylder kravene. Derfor tillader de fleste enhedstestrammeværker inklusive tSQLt gruppering af relaterede enhedstests i en enkelt testklasse i stedet for at køre individuelle tests.
AAA-princippet
Det er værd at nævne om 3-trins princippet for enhedstest, som er en standard praksis for at skrive enhedstests. AAA-princippet er grundlaget for enhedstestning og består af følgende trin:
- Arranger/saml
- Gør
- Bekræftelse
Arranger sektionen er det første trin i at skrive databaseenhedstests. Den guider gennem konfiguration af et databaseobjekt til test og opsætning af de forventede resultater.
loven sektion er, når et databaseobjekt (under test) kaldes for at producere det faktiske output.
Bekræftelse trin beskæftiger sig med at matche det faktiske output til det forventede og verificerer, om testen enten består eller ikke består.
Lad os udforske disse metoder på bestemte eksempler.
Hvis vi opretter en enhedstest for at bekræfte, at AddProduct gemt procedure kan tilføje et nyt produkt, opsætter vi Produktet og ExpectedProduct tabeller efter produktet er tilføjet. I dette tilfælde kommer metoden under sektionen Arranger/saml.
Kaldning af AddProduct-proceduren og indsættelse af resultatet i produkttabellen er omfattet af lovafsnittet.
Assert-delen matcher simpelthen produkttabellen med ExpectedProduct-tabellen for at se, om den lagrede procedure er blevet udført med succes eller mislykkedes.
Forståelse af afhængigheder i enhedstestning
Indtil videre har vi diskuteret det grundlæggende i databaseenhedstestning og vigtigheden af AAA-princippet (Assemble, Act, and Assert), når der oprettes en standardenhedstest.
Lad os nu fokusere på en anden vigtig brik i puslespillet – afhængigheder i enhedstestning.
Udover at følge AAA-princippet og kun fokusere på et bestemt databaseobjekt (under test), skal vi også kende de afhængigheder, der kan påvirke enhedstests.
Den bedste måde at forstå afhængigheder på er at se på et eksempel på en enhedstest.
EmployeesSample Database Setup
For at komme videre skal du oprette en eksempeldatabase og kalde den EmployeesSample :
-- Create the Employees sample database to demonstrate unit testing CREATE DATABASE EmployeesSample; GO
Opret nu medarbejderen tabel i eksempeldatabasen:
-- 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
Populering af prøvedata
Udfyld tabellen ved at tilføje nogle få 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 således ud:
-- View the Employee table SELECT e.EmployeeId ,e.NAME ,e.StartDate ,e.Title FROM Employee e; GO
Bemærk venligst, at jeg bruger dbForge Studio til SQL Server i denne artikel. Således kan output-looket variere, hvis du kører den samme kode i SSMS (SQL Server Management Studio). Der er ingen forskel, når det kommer til scripts og deres resultater.
Krav for at tilføje ny medarbejder
Nu, hvis et krav om at tilføje en ny medarbejder er blevet modtaget, er den bedste måde at opfylde kravet på at oprette en lagret procedure, som med succes kan tilføje en ny medarbejder til tabellen.
For at gøre dette skal du oprette AddEmployee-lagrede procedure som følger:
-- 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
Enhedstest for at bekræfte, om kravet er opfyldt
Vi skal skrive en databaseenhedstest for at verificere, om den lagrede procedure for AddEmployee opfylder kravet om at tilføje en ny post til Employee-tabellen.
Lad os fokusere på at forstå enhedstestfilosofien ved at simulere en enhedstestkode i stedet for at skrive en enhedstest med en testramme eller et tredjeparts enhedstestværktøj.
Simulering af enhedstest og anvendelse af AAA-princippet i SQL
Den første ting, vi skal gøre, er at efterligne AAA-princippet i SQL, da vi ikke kommer til at bruge nogen enhedstestramme.
Samling-sektionen anvendes, når de faktiske og forventede tabeller normalt er sat op sammen med den forventede tabel, der bliver udfyldt. Vi kan gøre brug af SQL-variabler til at initialisere den forventede tabel i dette trin.
Act-sektionen bruges, når den faktiske lagrede procedure kaldes for at indsætte data i den aktuelle tabel.
Assert-sektionen er, når den forventede tabel matcher den faktiske tabel. Simulering af Assert-delen er en smule vanskelig og kan opnås ved følgende trin:
- Tæller de fælles (matchende) rækker mellem to tabeller, som skal være 1 (da den forventede tabel kun har én post, der skal matche den faktiske tabel)
- Ekskludering af de faktiske tabelposter fra de forventede tabelposter skal være lig med 0 (hvis posten i den forventede tabel også findes i den faktiske tabel, så skal ekskludering af alle de faktiske tabelposter fra den forventede tabel returnere 0)
SQL-scriptet er som følger:
[expand title="Kode"]
-- 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
[/udvid]
Kører simuleret enhedstest
Når den lagrede procedure er oprettet, udføres den med den simulerede enhedstest:
-- Running simulated unit test to check the AddEmployee stored procedure EXEC TestAddEmployee
Udgangen er som følger:
Tillykke! Databasenhedstesten bestået.
Identifikation af problemer i form af afhængigheder i enhedstest
Kan vi opdage noget forkert i den enhedstest, vi oprettede på trods af, at den er blevet skrevet og kørt med succes?
Hvis vi ser nærmere på enhedstestopsætningen (Assemble-delen), har den forventede tabel en unødvendig binding med identitetskolonnen:
Før vi skriver en enhedstest, har vi allerede tilføjet 5 poster til den faktiske (medarbejder) tabel. Ved testopsætningen begynder identitetskolonnen for den forventede tabel således med 6. Det betyder dog, at vi altid forventer, at 5 poster er i den faktiske (Medarbejder) tabel for at matche den med den forventede tabel (#EmployeeExpected).
For at forstå, hvordan dette kan påvirke enhedstesten, lad os nu se på den faktiske (medarbejder) tabel:
Tilføj endnu en post til medarbejdertabellen:
-- Adding a new record to the Employee table INSERT INTO Employee (NAME, StartDate, Title) VALUES ('Mark', '2018-02-01', 'Developer');
Tag et kig på medarbejdertabellen nu:
Slet EmployeeId 6 (Adil), så enhedstesten kan køre mod sin egen version af EmployeeId 6 (Adil) i stedet for den tidligere gemte post.
-- Deleting the previously created EmployeeId: 6 (Adil) record from the Employee table DELETE FROM Employee WHERE EmployeeId=6
Kør den simulerede enhedstest og se resultaterne:
-- Running the simulated unit test to check the AddEmployee stored procedure EXEC TestAddEmployee
Testen mislykkedes denne gang. Svaret ligger i medarbejdertabellens resultatsæt som vist nedenfor:
Medarbejder-id-bindingen i enhedstesten som nævnt ovenfor fungerer ikke, når vi kører enhedstesten igen efter tilføjelse af en ny post og sletning af den tidligere tilføjede medarbejderpost.
Der er tre typer afhængigheder i testen:
- Dataafhængighed
- Nøglebegrænsningsafhængighed
- Identitetskolonneafhængighed
Dataafhængighed
Først og fremmest afhænger denne enhedstest af data i databasen. Ifølge Dave Green, når det kommer til enhedstestdatabasen, er selve dataene en afhængighed.
Det betyder, at din databaseenhedstest ikke bør stole på dataene i databasen. For eksempel bør din enhedstest indeholde de faktiske data, der skal indsættes i databaseobjektet (tabel), i stedet for at stole på de data, der allerede findes i databasen, som kan slettes eller ændres.
I vores tilfælde er det faktum, at der allerede er indsat fem poster i den faktiske medarbejder-tabel, en dataafhængighed, som skal forhindres, fordi vi ikke bør overtræde filosofien om enhedstest, der siger, at kun kodens enhed testes.
Med andre ord bør testdata ikke stole på de faktiske data i databasen.
Nøglebegrænsningsafhængighed
En anden afhængighed er en nøglebegrænsningsafhængighed, hvilket betyder, at den primære nøglekolonne EmployeeId også er en afhængighed. Det skal forhindres for at kunne skrive en god enhedstest. Der kræves dog en separat enhedstest for at teste en primær nøglebegrænsning.
For f.eks. at teste den lagrede procedure for AddEmployee, bør den primære nøgle til Employee-tabellen fjernes, så et objekt kan testes uden at bekymre dig om at krænke en primær nøgle.
Identitetskolonneafhængighed
Ligesom en primær nøglebegrænsning er identitetskolonnen også en afhængighed. Der er således ikke behov for at teste identitetskolonnens auto-increment-logik for AddEmployee-proceduren; det skal undgås for enhver pris.
Isolering af afhængigheder i enhedstestning
Vi kan forhindre alle tre afhængigheder ved at fjerne begrænsningerne fra tabellen midlertidigt og derefter ikke afhænge af dataene i databasen til enhedstesten. Sådan skrives standarddatabaseenhedstestene.
I dette tilfælde kan man spørge, hvor dataene til medarbejdertabellen kom fra. Svaret er, at tabellen bliver udfyldt med testdata defineret i enhedstesten.
Ændring af enhedstest lagret procedure
Lad os nu fjerne afhængighederne i vores enhedstest:
[expand title="Kode"]
-- 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
[/udvid]
Kørsel af afhængighedsfri simuleret enhedstest
Kør den simulerede enhedstest for at se resultaterne:
-- Running the dependency-free simulated unit test to check the AddEmployee stored procedure EXEC TestAddEmployee
Kør enhedstesten igen for at kontrollere AddEmployee-lagrede procedure:
-- Running the dependency-free simulated unit test to check the AddEmployee stored procedure EXEC TestAddEmployee
Tillykke! Afhængigheder fra enhedstesten er blevet fjernet.
Nu, selvom vi tilføjer en ny post eller et sæt af nye poster til Employee-tabellen, vil det ikke påvirke vores enhedstest, da vi har fjernet data- og begrænsningsafhængighederne fra testen med succes.
Oprettelse af databaseenhedstest ved hjælp af tSQLt
Det næste trin er at oprette en ægte databaseenhedstest baseret på den simulerede enhedstest.
Hvis du bruger SSMS (SQL Server Management Studio), skal du installere tSQLt-rammeværket, oprette en testklasse og aktivere CLR, før du skriver og kører enhedstesten.
Hvis du bruger dbForge Studio til SQL Server, kan du oprette enhedstesten ved at højreklikke på AddEmployee stored procedure og derefter klikke på "Unit Test" => "Add New Test..." som vist nedenfor:
For at tilføje en ny test skal du udfylde de påkrævede enhedstestoplysninger:
For at skrive enhedstesten skal du bruge følgende script:
-- 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 derefter databaseenhedstesten:
Tillykke! Vi har med succes oprettet og kørt databaseenhedstest, som er fri for afhængigheder.
Ting at gøre
Det er det. Du er klar til at isolere afhængigheder fra databaseenhedstest og oprette databaseenhedstest fri for data- og begrænsningsafhængigheder efter at have gennemgået denne artikel. Som et resultat kan du forbedre dine færdigheder ved at udføre følgende ting:
- Prøv venligst at tilføje Slet medarbejders lagrede procedure og opret en simuleret databaseenhedstest for Slet medarbejder med afhængigheder for at se, om den mislykkes under visse betingelser
- Prøv venligst at tilføje den lagrede procedure for Slet medarbejder og opret en databaseenhedstest fri for afhængigheder for at se, om en medarbejder kan slettes
- Prøv venligst at tilføje Search Employee-lagrede procedure og opret en simuleret databaseenhedstest med afhængigheder for at se, om der kan søges efter en medarbejder
- Prøv venligst at tilføje Search Employee-lagrede procedure og opret en databaseenhedstest fri for afhængigheder for at se, om der kan søges efter en medarbejder
- Prøv venligst mere komplekse krav ved at oprette lagrede procedurer for at opfylde kravene og derefter skrive databaseenhedstests fri for afhængigheder for at se, om de består testen eller fejler. Sørg dog for, at testen kan gentages og fokuserer på at teste kodens enhed
Nyttigt værktøj:
dbForge Unit Test – en intuitiv og praktisk GUI til implementering af automatiseret enhedstest i SQL Server Management Studio.