söndag 6 december 2009

En nybörjares syn på unit testing

EN sådär 10 år efter branschen är jag igång med lite unit testing. I den nya funktion jag nu ska implementera har jag försökt göra så kompletta unit tests som möjligt. Det är inte lätt.
Så här långt in i kampen har jag kommit fram till ett par saker:

Använd mocks - på rätt sätt
En Mock är ett fake-objekt, som man använder istället för den verkliga implementationen. Man ersätter då alla anrop på en verklig klass med anrop på en mock. Du kan då i testet styra vad din mock ska svara på anrop och på det viset skapa en känd omvärld för ditt test. Generellt sett har jag haft mest användning av mock objekt när det gäller de olika skiktens ställföreträdare och inte så mycket när det gäller affärsobjekt. Till exempel är det lättare att göra en fake implementation av en "ProduktManager", som innehåller funktioner för att hämta och spara produkter än för själva klassen "Produkt" i sig.

Men här märker man direkt om arkitekturen är testvänlig eller inte (läs: bra eller inte...). Om det inte är möjligt att byta ut sina "managers" mot andra implementationer är det svårt att använda mocks. Men om alla användare av managern använder ett interface är det enklare. Om man dessutom använder en instance factory eller en IOC containter (här är jag dock på tunn is) gör det hela lösningen ännu mer testbar.

Sen försöker jag dra gränsen för både mocks och omfattningen av test vid ett skikt. Det innebär att man testar ett skikt i taget och det enda som man "mockar" är skiktet under det skikt man just nu testar. (Phew, hur blev det där egentligen?).




Så, för att förklara:
Om jag ska skriva ett test för en coordinator, kommer jag att ge den en eller flera mocks för de managers den använder. På det sättet ger jag den klass jag använder givna förutsättningar. Inget konstigt så långt. Där drar jag gränsen för testet. Repository skikten är aldrig inblandat i testet. Inte heller GUI. Det gör testet mindre, mer fokuserat och enklare att skriva.

Behovet av att använda mocks för andra objekt, som entiteter har jag ännu inte riktigt utrett. Det jag har märkt är att det kan hjälp att skapa enkla metoder för att fylla på de riktiga affärsobjekten med testdata. Men jag använder då i vilket fall det riktiga affärsobjektet. Men vem vet, kanske din hjälpfunktion är en indikation på att du behöver den även i produktionskoden?

Ett sätt att bemästra GUI tester... lite
Hur man testar GUI är ett problem för mig. Men jag har hittat en utväg. Gör GUI:t dummare.

Med en coordinator (som i vårt fall är den GUI pratar med) som levererar så färdig information som möjligt kan man lägga mer kod i den mycket mer testbara coordinatorn. Att tänka på vad som är enklast att hantera för ett GUI, och bygga coordinatorn efter det, gör skillnad.

Funktioner som exempelvis tar in strängvärden kan man evaluera om värdet var giltig integer eller ej i coordinatorn. Sen får GUI:t hantera det som den vill, men problemet kommer inte sprida sig vidare eftersom vi med tester i coordinatorn sett till att alla felaktiga värden hanteras korrekt.

Ett annat bra sätt är att helt ta bort null-problematiken för GUI:t. Låt aldrig coordinatorn returnera null, utan istället en tom lista, tom sträng etc. Kanske är det till och med så att vi borde skapa VårKlass.Empty för att alltid ge tillbaka något. Hur många NullReference problem har man inte fått...

Vid databindning i en DataGridView, använd antingen CellValueNeeded och CellValuePushed som pekar på GetValue(...) i coordinatorn och SetValue(...) i coordinatorn. Eller, vilket oftast är ännu bättre, skapa tillrättalagda, testbara GUI objekt com är gjorda för databindning.

Kontentan är att GUI:t ska vara så dumt som möjligt! Då slipper vi komma på obskyra sätt att testa det med unit tests. Då kan GUI:t testas manuellt utifrån användbarhet och inte för att hitta buggar med vad jag ibland kallar för "Happy testing". Klicka här, klicka där... klicka lite här.... Ja du vet vad jag pratar om.

Håll testerna rena och tydliga
När TDD förespråkarna gav oss utvecklare fribrev vad gäller dokumentation hurrade nog inte bara jag. "Jag skriver kod istället för dokumentation - grymt!". Men då gäller det ju att testerna går att läsa som dokumentation. Här har jag hittat ett par bra tips
  • Låt testerna få tydliga namn, hur långa som helst bara de är tydliga
  • Uttryck testerna som ett krav eller påstående, som SökningPåOkändProduktGerTomLista eller IckeNumerisktVärdeFörAntalPåverkarInteDataOchKastarInteException.
  • Undvik onödiga Asserts i testet. Om du har gjort en assert i ett testfall, gör inte om den i ett annat. Till exempel, du har en funktion som alltid ska returnera resultat och inte null. Säkerställ detta i testet HämtaProdukterReturnerarAlltidEttVärde. I alla andra tester förutsätter man sedan att så är fallet. Inga if != null, inga Assert.IsNotNull, såvida det inte gäller något specifikt för just det nya testet.
  • Skapa hjälpfunktioner i din klass som gör komplexa eller otydliga operationer.
    Jag råkade ut föra att jag i många tester behövde hämta ut ett objekt från en IEnumerable med hjälp av LINQ. Koden blev rätt hårig och var egentligen inte del av testet. Istället för att skapa massa komplex kod i mitt test la jag ut den koden i en hjälpfunktion med enkelt namn. Då ser testet snyggare ut och blir enklare att förstå. Dessutom är det ju inte fel att lägga ett test på hjälpfunktionen också, en gång för alla så att inte heller resultat från den behöver ifrågasättas.
  • Använd alltid möjligheten till att skriva en kommentar till din Assert, alltså en sista inparametern till Assert, inte som en kommentar i koden.
  • Skriv aldrig kommentarer i testkoden. Den ska inte behöva kommenteras.
  • Ta utan att blinka bort tester som heter TestMethod1 eller TestaProdukt mm. Vad gör du när ett sånt test fallerar? Nä, dåligt namngivna tester är faktiskt sämre än inga tester, iallafall ur dokumentationssynpunkt.
  • Låt dina Asserts återspegla vad testet heter, dvs det ska var tydligt att din Assert testar just det som namnet på ditt test säger
  • Klappa dig själv på axeln om du gör ett test med bara EN assert i. Bra gjort!
Med andra ord - ställ samma krav på testkoden, eller högre(!), som på produktionskod vad gäller tydlighet och format.

TDD eller... ?
Precis som med Unit tests är det ju inte direkt ett buzzword längre. TDD ha funnits ett bra tag, men jag har inte lyckats ta det till mig än. Jag tycker Uncle Bob är en riktig hjälte med många bra idéer. Jag skulle gärna vilja känna att TDD är min grej. Du vet väl vad TDD reglerna är?
  • You are not allowed to write any production code unless it is to make a failing unit test pass.
  • You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
  • You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
Mitt förfarande för unit tests är just nu trevande och utforskande. Det består i stort i att koda i mindre sjok för att sedan skriva tester i mindre sjok och så börja om igen. Istället för att känna mig missnöjd över att jag ännu inte kör TDD väljer att se det så här: Nu gör jag tester, det gjorde jag inte förut. Mina kodsvängar är kortare nu och jag tänker hela tiden på testbarheten. Jag är på gång!

Uncle Bob själv sa sig vara lite skeptisk till TDD först, men blev helt såld på det. Jag gillar ett citat han gav angående insikterna efter att ha givit sig hän till TDD, det säger lite av min känsla också.
I can no longer concieve of typing in a big long batch of code hoping it works. I can no longer tolerate ripping a set of modules apart, hoping to reassemble them and get them all working by next Friday.
- Uncle Bob

Over and out!

Dagens  kodarmusik: Supersnälla Silversara - Samla Samla (okej udda, men den är OMÖJLIG att få ur huvudet!!! Prova - jag vill inte lida själv.)


Lite länktips
Know Your Units - Kevlin Henney, Curbralan, UK
Coplien och Martin debaterar TDD, CDD och professionalism
TDD artikel från mästaren själv, Uncle Bob

Inga kommentarer:

Skicka en kommentar