Unit Testing behøver ikke at være besværligt

Published: 18. April 2010

Det kan være besværligt at lave Unit Tests - især hvis du prøver at komme igang med det. Der står uendelige mængder om Unit Testing på nettet, men de har det tilfælles at de tit har mere fokus på at lave meget løs koblet arkitektur, end på at teste. I denne post, vil jeg vise at der også er mere simple måder at gøre tingene på, og at "Lav et interface" ikke altid er svaret på alle spørgsmål.

Unit Tests er efterhånden slået godt igennem forskellige steder. Næsten alle er enige om at det nok er den rigtige vej at gå, men der en meget lille del af os alle som bruger det. Jeg høre til den gruppe der synes det er en god ide, men får det bare ikke brugt.

Så hvorfor er det sådan?

Det korte svar er fordi det er for besværligt.

Lad os sige, at jeg har et PeopleRepository, som henter folk ud fra databasen, og sortere dem efter efternavn. Nu vil jeg så gerne teste at den nu også sortere dem rigtigt. Hvis vi skal følge den "Best Practice" som flyder rundt på nettet, så vil dette betyde at PeopleRepository nu skal til at snakke med en anden klasse som laver database kaldende. Denne kan vi kalde PeopleDataContext. Da vi skal fake denne, så skal vi have et interface, det kalder vi IPeopleDataContext, og dette har en metode der heder GetPeople, som bare henter alle personer ud fra dit elskede ORM. Det er så PeopleRepository's ansvar at sortere disse.

Nu skal PeopleRepository jo så have en IPeopleDataContext - og alle ved jo, at den eneste rigtige måde at gøre dette på er via en IoC container.

Så vi henter vores yndlings IoC container, og sætter denne op til at kunne spytte PeopleDataContext ud, når folk spørger efter interfacet..... Smart, men jeg er stadig ikke klar til at teste.

Nu skal jeg jo så have en fake af IPeopleDataContext, som retunere nogle hardcoded personer. Jeg henter da mit yndlings Mocking framework, og bruger tid på at sætte dette op... Sådan

Nu er vi oppe på at en klasse med en lille bitte metode, er blevet til 2 klasser, 1 interface, og 1 IoC Framework, og 1 Mocking framework, og selvfølgelig et Unit Testing Framework.

Hvis jeg så stadig kan huske, hvad det var jeg gerne ville teste, så er jeg nu klar til at skrive min unit test, som omhandler, at jeg skal sætte noget data op, opsætter mit Mocking Framework, og laver mine Assertions. Hvis du er heldig, så får du grønt lys, og kan nu gå hjem som en glad mand (eller dame)

Nu skal det siges at alt dette også kommer med en fordel: Jeg kan f.eks. skifte min DataContext ud, hvis jeg har behov for det. Det kan være smart - men med mindre dette er et krav, eller bliver et krav, så er dette "spilt" arbejde.

Brug hovedet, ikke Google (...eller Bing)

Det skal ikke være nogen hemmelighed, at jeg er imod at gøre Unit Testing besværligt, eller få Unit Testing til at betyde, at min arkitektur nu betyder at jeg skal til at vedligeholde et interface, og en masse bi-produkter af lav kobling, bare for at jeg kan unit teste - især ikke, når der findes "nemmere" måder at gøre dette på.

Jeg er helt med på, at denne måde at gøre tingene på, har sin plads, og for Unit Testing guru'en, så er dette også en naturlig del af hans arbrejdsflow. Men for den lille newbie, så er dette bare alt for meget, til at man kan få den gode fornemmelse af, at Unit Testing nu også "gav" nok.

Jeg har en regel der heder, at hvis en klasse har én "rigtig" implementation, så er denne klasse per definition dens eget interface. Interfaces er til, hvis det der er behov for applikationen at udskifte dette komponent helt, samtidig med at det andet komponent stadig skal bruges. Dette kunne f.eks. være hvis din applikation har behov for at kunne kobles op mod både MySql og MsSql - jamen, så lad da et interface. Men hvis din applikation p.t. bruger MySql og en dag skal skrifte helt til MsSql, så skal du ikke lave et interface. Hvorfor? Fordi det eneste rigtige må være at fjerne din MySql kode, og istedet skrive din MsSql kode. Du ønsker jo aligevel ikke at holde din MySql support i live, da du ikke har noget krav om at det nogensinde skal køre på MySql igen. Derfor har du stadig kun én "rigtig" implementering, og den er jo så interfacet per definition.

Hvad betyder det for vores Unit Tests? Ja det betyder, for det første, at vi nu ikke behøver IPeopleDataContext, og måske ikke vores PeopleDataContext, hvis vi ikke har behov for afkoblingen.

Den simpleste måde, er nu at lave en lille refactoring i vores PeopleRepsitory, så vi nu har en metode som henter people fra databasen, og vores metode, som kalder denne metode, og sortere og retunere tilbage. Her er hele min kode:

public class Person
{
    public string Lastname { get; set; }
}

public class PeopleRepository
{
    public virtual IEnumerable<Person> ListPeople()
    {
        return from person in GetAllPeople()
               orderby person.Lastname
               select person;
    }
    public virtual IEnumerable<Person> GetAllPeople()
    {
        return new List<Person>(); //Indsæt dit eget ORM/DB Kald her
    }
}

[TestClass]
public class PersonRepositoryTest : PeopleRepository
{
    public override IEnumerable<Person> GetAllPeople()
    {
        return new List<Person>()
                   {
                       new Person() {Lastname = "Jensen"}, 
                       new Person() {Lastname = "Olsen"}, 
                       new Person() {Lastname = "Andersen"}
                   };
    }

    [TestMethod]
    public void ListPeople_MustBeOrderedByLastName()
    {
        var result = ListPeople();
        // Ja, det er ikke jordens bedste måde at teste sortering på :)
        Assert.AreEqual("Andersen",result.First().Lastname);
        Assert.AreEqual("Olsen", result.Last().Lastname);
    }
}

Bemærk at jeg laver flere underlige ting her. For det første er alle mine repository metoder virtual. Dette gør selvfølgelig at jeg kan override dem. Bemærk også, at istedet for at lave et Fake object, eller lave et Mock object, så er min test en nedarvning af min repository klasse, og min test klasse er derved også min fake. Bemærk at dette måske er lidt ekstremt, og kun virker for klasser som ikke har noget behov for en contructor som tager argumenter. Men det virker, og er dejligt nemt, og præcist.

Så som du kan se, så er det ikke vanvittigt svært at skrive en simpelt unit test, uden alle de klasser og overhead.
Om du skal gå helevejen som jeg har gjort her, kommer an på hvad dine behov er i den kode du tester, men jeg har lavet dette for at vise at unit tests, og testbar arkitektur ikke behøver at betyde en masse besvær. Husk at der findes mange mellemveje fra mit kodeeksempel og så hele vejen op til "hele pakken" med interfaces, og IoC containers - find den mellemvej som giver mening for dig.

Som bonus skal det siges denne måde at implementere dette på, faktisk også er let i forhold til eksisterende kode, da det stortset kun kræver at du laver dine metoder virtual, og sørger for at kode som du skal fake, har sine egne metoder.

Så hvis du stadig ikke laver unit tests, fordi det er for besværligt, så er dette hermed en opfordring til at bruge hovedet, og ikke nødvendigvis lave alle de ekstra klasser, og alt det ekstra arbejde, for at teste din kode. Det behøver nødvendigvis ikke være "alt eller intet"

Comments

Write a comment


You can use Markdown formatting