Jeg har på det seneste kastet mig ud i at teste Data Access
Layer i et projekt. Projektet er baseret på SQL Server med Linq2Sql
som ORM.
Jeg har længe udskudt denne aktivitet, da det virkede
uoverkommeligt at skulle håndtere databasetilstand før og efter
tests og har i det hele taget været i tvivl om hvordan jeg skulle
gribe det an, med Linq2Sql som ORM. Nu har jeg taget tyren ved
hornene og kastet mig ud i det. I den forbindelse har jeg gjort
nogle foreløbige erfaringer som jeg lige vil lave et lille notat
om...
Isolering af databasen
Som udgangspunkt vil jeg gerne have adskilt min testdatabase fra
den database jeg benytter til at lave lokale udforskende tests på
(dvs. de tests jeg laver når jeg kører det site jeg arbejder med).
Dette er dels for ikke at fucke de data jeg sidder og laver mens
jeg tester udforskende, dels for at kunne have "ren røv at trutte
i", til mine automatiske tests.
Derfor har jeg replikeret skemaet fra min udviklingsdatabase og
oprettet en dedikeret autotest-database, som jeg så peger på fra
min testprojekts app.config.
Før hver test kører jeg en wipe af databasen. Dette gøres fra en
metode dekoreret med AssemblyInititalize, hvor jeg udfører
truncate/delete på alle relevante tabeller.
Derefter sætte jeg nogle basale data ind i databasen, da der er
nogle data som skal være til stede for at systemet kan komme igang
(f.eks. en bruger der kan refereres til fra mange andre
tabeller).
Opbygning af tests
Når man så skal igang med at skrive tests, er det vigtigt at
tænke på samtidigheden i udførslen af tests, hvilket bla. betyder,
at man ikke altid kan være sikker på, hvilke data der ligger i
databasen på et givet tidspunkt. Derfor kan tests ikke blindt gå ud
fra at der er et specifikt antal elementer i databasen, da flere
tests, på et vilkårligt tidspunkt i testforløbet, kan oprette eller
slette data.
Tests bør såvidt muligt teste for tilstande, som er statiske fra
gang til gang, dvs. hvis en funktion der returnerer en liste af
elementer ud fra et eller andet kriterium, så bør man teste om
dette kriterium er opfyldt i alle de returnerede elementer i stedet
for at teste om et bestemt antal er returneret (med mindre det rent
faktisk er et kriterium for testen).
Et eksempel på hvordan man kan gøre dette, kunne se således
ud:
var target = new PersonSQLRepository(GetLocator());
List<IPerson> actual = target.GetPersonList(true);
Assert.IsTrue(actual.TrueForAll(p => p.Activated));
GetLocator er blot en funktion der returnerer en Service
Locator, der benyttes til at oprette IPerson-objekter inde i
PersonSQLRepository-klassen. Parameteren til GetPersonList angiver,
om personen skal være aktiveret.
Hvis det f.eks. havde være en test af paginering af det
returnerede data, ville det være OK at teste om antallet af
elementer oversteg det ønskede antal og evt. om det totale antal
gav anledning til at testen burde fejle.
Et eksempel på dette kunne se således ud:
var total = 0,
target = new PersonSQLRepository(GetLocator());
List<IPerson> actual = target.GetPersonList(10, 1, out total, true);
Assert.IsTrue(total>=10 ? actual.Count==10 : actual.Count==total);
Assert.IsTrue(actual.TrueForAll(p => p.Activated));
GetPersonList's parametre betyder: antal elementer der ønskes
returneret, sidenr, antal relevante elementer ialt i databasen,
samt kriteriet fra tidligere med kravet om aktiverede personer.
Total indeholder, efter kaldet, det faktiske antal personer der
møder kriteriet.
Som det ses tages der stilling til forskellige scenarier
f.s.v.a. antal returnerede personer, hvilket her er OK, da kravene
til værdisættet er statisk.
Opsummering
Ifm. med test af DAL bør databasen isoleres og man skal lave
sine tests defensivt, dvs. ikke gøre antagelser om det datasæt man
arbejder med, men blot forsøge at forholde sig tilstanden af de
data der returneres holdt op mod de forventninger man har til
data.