|
|
Interfaces kontra nedarvning i unittestsJeg er begyndt at lege lidt med unittests i Visual Studio 2008 og har forud for dette bla. læst et par bøger om unittests og testdriven development (TDD). I disse bøger har man talt for isolerering af afhængigheder til andre klasser i forbindelse med tests vha. interfaces. Nu er jeg så igang med at klaske noget kode sammen, som afspejler et mindre hierarki af klasser. Der er i den forbindelse nogle afhængigheder disse klasser imellem og dem har jeg forsøgt at isolere ved at definere interfaces hvor igennem klasserne "snakker" med hinanden. Jeg laver pt. disse interfaces manuelt og stubber ligeledes manuelt. Det er imidlertid gået op for mig, at der går lidt benarbejde med at opdatere både forretningsklassen og stubklassen, når jeg skal udvide et interface. Dette mener jeg umiddelbart at kunne undgå, hvis jeg nedarver forretningsklassen i stubklassen. Jeg er klar over, at jeg så er nød til at gøre metoder virtuelle i forretningsklassen, men det synes jeg er en mindre risiko, når nu hierarkiet er til eget brug (og så fremmer det jo testbarheden af mine klasser :-)). Jeg kan givetvis spare en del af dette manuelle arbejde ved at benytte et mockingbibliotek (så som NMock, moq eller TypeMock Isolator), men til mit nuværende behov (og pengepung for såvidt angår TypeMock Isolator), er det fint at kode stubbe manuelt - selvom jeg på et tidspunkt nok bliver nød til at kaste mig over noget mockinglib af en art :-). Test ved brug af interfaceHvis nu jeg tager udgangspunkt i et setup, hvor jeg vil teste om en person må tilmelde sig et kursus på grundlag af nogle forudsætninger, som kurset undersøger for en given person, så kunne klassen til mit kursus og mit interface til personen se således ud:
class Kursus
{
void Tilmeld(IPerson person)
{
if(person.HarBestaaetTest(1) && person.HarBestaaetTest(2))
{
// så skal personen blot tilmeldes... sikkert igennem et andet interface til
// et repository - dette undlades lige i dette eksempel.
}
else
{
// Se lige bort fra at det er en generisk exception der smides,
// meningen er vist klar nok ;-)
throw new Exception("Personen er ikke kvalificeret til at deltage på kurset.");
}
}
}
interface IPerson
{
bool HarBestaaetTest(int testId);
}
Meningen er så at Kursus.Tilmeld kalder IPerson.HarBestaaetTest med id'et for den test som skal være bestået for at undersøge om denne person må deltage på kurset. Hvis personen er kvalificeret, så tilmeldes personen, ellers rejses der en kørselsfejl. Et helt simpelt setup, som burde være til at forstå :-) Nu har jeg så dels min rigtige implementation af Person, dels min stubimplementation. Den rigtige implementation af Person kunne se således ud:
class Person : IPerson
{
public List<int> Karakter = new List<int>();
public bool IPerson.HarBestaaetTest(int testId)
{
return Karakter("test" + testId) >= 6;
}
}
I min test vil jeg gerne løsrive mig fra evt. afhængigheder der måtte være forbundet med Person-klassen, så derfor stubbes klassen til følgende klasse:
class PersonStubBestaaet : IPerson
{
public bool IPerson.HarBestaaetTest(int testId)
{
return true;
}
}
Med min stub har jeg sikret mig at personen har bestået alle tests der kræves af kursus og kan defor forvente at et kald til Kursus.Tilmeld vil lykkes, når den kaldes med en forekomst af min stubbede Person-klasse. Min test kunne se således ud:
[TestMethod()]
public void Kursus_TilmeldPerson_TilmeldingOK()
{
Kursus kursus = new Kursus();
IPerson person = new PersonStubBestaaet();
bool testOK = false;
kursus.Tilmeld(person);
testOK = true;
Assert.IsTrue(testOK, "");
}
Hvis jeg nu vil teste om kurset fejler, hvis en eller flere tests ikke er bestået, kunne jeg flikke denne stub af person-klassen sammen:
class PersonStubEjBestaaet : IPerson
{
public bool IPerson.HarBestaaetTest(int testId)
{
return false;
}
}
Dette kunne se således ud i testen:
[TestMethod()]
public void Kursus_TilmeldPerson_TilmeldingFejlerMedException()
{
Kursus kursus = new Kursus();
IPerson person = new PersonStubEjBestaaet();
bool testOK = false;
try
{
kursus.Tilmeld(person);
}
catch(Exception x)
{
if(x.Message == "Personen er ikke kvalificeret til at deltage på kurset.")
testOK = true;
}
Assert.IsTrue(testOK, "");
}
Således kunne jeg fortsætte med at teste forskellige scenarier af Tilmeld-funktionen på Kursus-klassen og jeg kunne ende med en række stub-implementeringer af Person alene til understøttelse af testen af denne ene funktion. Jeg er klar over at disse eksempler er ret simple og at jeg sagtens kunne tilføje lidt egenskaber til min stub, så alle scenarier kunne benytte samme stub, men der findes nok scenarier, hvor dette ikke er så simpelt, så bær over med mig for en stund... :-) Udfordringen her ville så være, hvis jeg senere skulle tilføje en funktion til mit Person-interface. Dette ville nemlig medføre at jeg skulle igennem alle de klasser som implementerer IPerson for at implementere denne funktion (hvilket kan være noget irriterende for at kunne komme videre med en test som egentlig ikke vedrører disse andre implementeringer). Dette kunne nedarvningen måske hjælpe med at undgå. Test ved brug af nedarvningGivet klasserne Kursus og Person ovenfor, kunne jeg ændre implementeringen af Person til at se således ud:
class Person
{
public List<int> Karakter = new List<int>();
public virtual bool HarBestaaetTest(int testId)
{
return Karakter("test" + testId) >= 6;
}
}
Kursus-klassens implementering af Tilmeld skal rettes til benytte Person i stedet for IPerson, dvs. funktionen erklæres med denne header i stedet:
void Tilmeld(Person person) { ... }
Således har jeg åbnet for at en nedarvet klasse kan overstyre funktionen HarBestaaetTest og implementere den som det måtte være passende (f.eks. til bare at returnere true eller false, som det gøres i stubbene fra forrige afsnit). Min stub kunne nu implementeres således:
class PersonStubBestaaet : Person
{
public override bool HarBestaaetTest(int testId)
{
return true;
}
}
Testen kunne så se således ud:
[TestMethod()]
public void Kursus_TilmeldPerson_TilmeldingOK()
{
Kursus kursus = new Kursus();
Person person = new PersonStubBestaaet();
bool testOK = false;
kursus.Tilmeld(person);
testOK = true;
Assert.IsTrue(testOK, "");
}
Som det ses, er dette eksempel klaret med lidt færre linier kode og noget mindre vedligehold i forbindelse med udvidelse af Person-klassen, end ved implementering af et interface. Dog kan man sige at klassen har slækket lidt på sikkerheden i og med HarBestaaetTest nu er virtual og dermed kan nedarves og ændres af folk med mindre reelle hensigter. I mit tilfælde vurderer jeg dog dette til at være en risiko jeg godt vil løbe, da det kun er mig selv der skal bruge disse klasser og dermed må forventes at have nogenlunde reelle hensigter med mit system... :-) KonklusionDet er klart at mit eksempel er ret simpelt og at denne artikel er baseret på gangske lidt erfaring med Unittests, så der vil nok være mange af jer Unittest-guruer, som ryster på hoved på vej op mod Back-knappen, men jeg betragter blot verden fra den ø af uvidenhed (eller er det et kontinent?), som jeg i øjeblikket sidder på. Jeg regner med at den bliver mindre med tiden og at jeg dermed får våde ankler =) |
| Sidst opdateret: 14-10-2009 08:41:54 |
|
Tilmeld link |
Tilføj Link |
Tilføj Link |
@-begynder Erklæring om beskyttelse af personlige oplysninger |