Joakim Holm hos Avega: TDD i praktiken
Den bloggande terriern Joakim Holm var den sista i raden av föredragshållare som besökte Avega innan vi nu gör ett kort uppehåll över sommaren. Joakim Holm är en systemutvecklingskonsult med fokus på agila processer som Extreme Programming och Scrum. Han var inbjuden till Avega för att leda en kata för testdriven utveckling (TDD) i Java. "Kata" är ett uttryck från japansk kampsport där det är liktydigt med ett koerograferat mönster av rörelser, t.ex. slag och sparkar. I detta sammanhang syftade det väl närmast på en koreograferad TDD-utvecklingssession där Holm vid tangentbordet med fast hand lotsade oss igenom hela övningen.
Holm tog sin utgångspunkt i Robert C. Martins (alias "Uncle Bob") exempel med ett program som ska beräkna poäng för en bowlingserie. Detta exempel återfinns i den eminenta boken "Agile Software Development, Principles, Patterns, and Practices" (finns även i nyutgiven version för C#), men Holms förlaga var om jag förstod saken rätt en live-dragning av Robert Martin själv som skiljde sig lite från (mitt minne av) boken. Kraven är lika enkla som uppenbara: poängberäkningsreglerna för bowling. Först av allt tas en enkel design fram, ett UML-diagram med några till synes självklara klasser: Game, Roll, Frame och Score. Som sig bör i TDD börjar man koda genom att först skriva ett så enkelt test som möjligt:
public void testCanCreateGame {
BowlingGame bowlingGame = new BowlingGame();
assertNotEquals(null, bowlingGame);
}
Eftersom detta är den första kod vi skriver kan vi inte ens kompilera - det finns ju ingen BowlingGame-klass. Med hjälp av refaktoriseringsverktyget ("refactoring tool") i vår utvecklingsmiljö (Eclipse för Java i det här fallet, för Visual Studio måste man använda ett tredjepartstillägg som ReSharper) skapar vi enkelt klassen automatiskt. Efter det kan vi köra vår testsvit, som än så länge bara består av ett test, och få grönt ljus när testet passerar. Nästa test blir lite svårare, men är fortfarande väldigt enkelt: poängen när alla slag hamnar i rännan ska vara noll. Eftersom man maximalt kan slå 21 slag i en serie borde testet se ut så här:
public void testGutterGame {
BowlingGame bowlingGame = new BowlingGame();
for(int i = 1; i <= 21; i++) {
bowlingGame.roll(0);
}
assertEquals(0, bowlingGame.score());
}
Testet kompilerar naturligtvis inte, utan vi får lägga till metoderna roll(int) som tar antal nedslagna käglor som inparameter och score() som returnerar den totala poängen i spelet. Denna gång lägger vi emellertid till ett viktigt element i TDD som vi hoppade över i det första testet; när vi fått programmet att kompileras ser vi till att få rött ljus på testet. Vi kodar vår score()-metod så att den returnerar -1 bara för att få testet att misslyckas. Om vi inte gör det kan vi inte vara säkra på att testet verkligen testar det vi tänkt. När vi fått rött ljus ändrar vi "produktionskoden" så att den returnerar summan av de 21 slagen och vi får grönt ljus. Här framhåller Holm en viktig regel i TDD: innan vi får skriva en enda rad produktionskod måste vi få rött ljus från ett test. Därför är det positivt med rött ljus - då får vi koda!
En annan regel i TDD är att alltid välja den enklaste lösningen för att få ett test att passera och aldrig skriva mer kod än nödvändigt. Sålunda tänker vi inte på spärr och strike när vi skriver vår score()-metod utan summerar alla slag rakt av (vid spärr och strike får man en bonus som är lika stor som ett eller två nästföljande slag). Vi bryr oss inte heller om duplicering och funderar kanske inte så mycket på att bryta ut koden i egna metoder. Detta gör vi istället när vi har ett passerande test på plats - då kan vi refaktorisera vår kod steg för steg och köra testerna för att se att vi fortfarande får grönt ljus. Detta sätt att arbeta gör att man med stor trygghet gradvis kan förbättra designen i systemet utan att behöva oroa sig för att bryta existerande funktionalitet.
För att sammanfatta ser stegen för testdriven utveckling ut så här:
- Skriv ett test som misslyckas (rött ljus).
- Se till att det kompilerar.
- Skriv kod så att testet lyckas (grönt ljus).
- Ta bort duplicering och förbättra design.
- Repetera.
Följandes detta mönster gick vi tillsammans med Holm framåt steg för steg till dess att vi slutligen hade ett program som klarade att beräkna poängen för bowlingserier med såväl rännslag som spärr och strike. Förutom att testerna hjälper oss att gå framåt och är ovärderliga vid förvaltning och vidareutveckling av programmet, gav de oss också en applikationsdesign som var en annan än den vi från början skissade fram. Faktum är att BowlingGame blev applikationens enda klass och att den hade ett mycket begränsat antal metoder som dessutom var korta och koncisa. Inledningsvis var det nog ingen som hade trott att programmet skulle bli så litet och enkelt och hade vi inte använt oss av TDD hade det förmodligen inte blivit det heller. Jag har själv sysslat en del med TDD på sistone och gillar det verkligen. Något som jag dock inte riktigt kopplat greppet om var just den "top-down-design" som Holm/Martin använder. Genom att börja uppifrån med användarcentrerade funktioner som "testGutterGame", snarare än börja på låg nivå med t.ex. Frame.GetScore() om vi utgått från det initiala UML-diagrammet, blir det lättare att undvika överdesign.
Jag är övertygad om att jag kommer att jobba allt mer med testdriven utveckling i framtiden och Holms roliga TDD-kata inspirerade mig ytterligare. Om du ännu inte testat TDD råder jag dig att göra det. Det kan vara en liten tröskel innan man kommer i gång, särskilt om man inte är van att jobba på det objektorienterade sätt som i alla fall jag har svårt att separera från TDD och det kan även vara svårt att vänja sig vid tanken på att skriva ett test innan det finns kod, men man kommer snabbt över den och då är det verkligen ett roligt och belönande arbetssätt.

1 kommentar(er):
Hej Joakim. Jag läste dina intryck av seminariet med stort intresse, trevligt. Bra sammanfattat! Tack också för bra frågor under seminariet.
Skicka en kommentar