lørdag 23. mai 2009 Softwareutvikling Testing / TDD
Det er snart et år siden Dan North var i Bergen og snakket om Behaviour-Driven Development (BDD) - et besøk jeg bl.a. dokumenterte i the Contiki Strip #5. Det var et utrolig inspirerende møte, og Given-When-Then spesifikasjonformatet var noe vi prøvde å jobbe med i tiden etterpå. Men siden vi ikke hadde kommet orntlig igang med å skrive enhetstester, og var langt fra å praktisere TDD, glemte vi det raskt ut, og alt forble ved det gamle.
Nå som jeg har jobbet mer med testdreven utvikling er det på tide å ta dette opp igjen. Jeg vil derfor presentere hvordan man kan gå fra "vanlige" unit-tester til å implementere kjørbare spesifikasjoner (the BDD way), og snakke litt om hvilke fordeler dette gir oss.
Ta f.eks. denne enhetstesten, hentet fra Robert C. Martins bok Agile Principles, Patterns, and Practices in C#:
1 [Test]
2 public void DeleteEmployee()
3 {
4 int empId = 4;
5 AddComissionedEmployee t =
6 new AddComissionedEmployee(empId, "Bill", "Home", 2500, 3.2);
7 t.Execute();
8
9 Employee e = PayrollDatabase.GetEmployee(empId);
10 Assert.IsNotNull(e);
11 DeleteEmployeeTransaction dt = new DeleteEmployeeTransaction(empId);
12 dt.Execute();
13
14 e = PayrollDatabase.GetEmployee(empId);
15 Assert.IsNull(e);
16 }
Denne koden har utstract bruk av COMMAND pattern. Testen oppretter først en Employee i databasen (line 5-7), og bekrefter at den ble opprettet (line 9-10). Deretter sender den en slette-komando (linje 11-12) og verifiserer at den nå ikke finnes i databasen lengre (linje 14-15). En helt normal enhetstest etter min mening.
Den uttrykker derimot ikke så mye om intensjonen bak testen - man må lese koden for å forstå hva som skjer, og strukturen er helt og holdent opp til utvikleren. En bedre måte å gjøre dette på er å sette opp et Given-When-Then-senario: Gitt at vi har en kunde, Når vi ber om å få den slettet, Så finnes den ikke lengre i databasen.
Implementert vha. TinyBDD, et rammeverkt for å lage BDD-spesifikasjoner under utvikling av Gjøran Hansen, forvandles enhetstesten til noe sånn som dette:
1 [Test]
2 public void DeleteEmployeeTransaction_should_remove_employee()
3 {
4 int empId = 4;
5
6 Scenario.New("DeleteEmployeeTransaction should remove employee",
7 scenario =>
8 {
9 scenario.Given("An Employee with a specific ID", () =>
10 new AddComissionedEmployee(empId, "Bill", "Home", 2500, 3.2).Execute())
11
12 .And("It exists in the repository", () =>
13 Assert.IsNotNull(PayrollDatabase.GetEmployee(empId)));
14
15 scenario.When("A delete transaction is sent with that ID", () =>
16 new DeleteEmployeeTransaction(empId).Execute());
17
18 scenario.Then("The Employee should no longer exist in repository", () =>
19 Assert.IsNull(PayrollDatabase.GetEmployee(empId)));
20
21 }).Execute();
22 }
Om du ikke er vandt med lambda-uttrykk i C# er dette sikkert ganske gresk, men gi det en sjanse likevel. I linje 6 opprettes et nytt senarie. Så på linje 9 sier vi at hvis (Given) en bestemt Employee existerer, og (på linje 12) han finnes i databasen, når (When) man da eksekverer en slette-kommando (linje 15), så (Then) vil Employee'en ikke lenger finnes i basen (linje 18).
Lambda-uttrykkene på linje 10, 13, 16 og 19 utfører og/eller tester det vi har spesifisert i kallene til Given, When og Then. Og sammenligner du med den originale testen så ser du at de egentlig gjør akkurat det samme.
Forskjellen er at BDD-spesifikasjonen beskriver/dokumenterer i klartekst hva situasjonen er og hvilken adferd vi er ute etter. Og spesifikasjonen er så nær koden at det ikke vil være noe problem å holde dem og koden synkronisert om den skulle endres. Den er relativt oversiktelig, og følger et fast mønster.
Og den virkelige gevinsten oppdager du når du innser at disse spesifikasjonene kan skrives av eller i sammarbeid med testere, product owners og til og med kunden selv. Spranget fra kundens eller produktets krav til selve koden blir minimal. Utvikleren og kunden kan f.eks. sitte sammen og skrive spesifikasjonen rett inn i en .cs-fil.
Når spesifikasjonene er definert kan man så kjøre dem underveis i prosjektet, og ta ut rapporter som i klartekst beskriver senariene, og hvilke av dem som er ferdig implementert, hvilke som gjenstår, og eventuelt hvilke som feiler (TinyBDD støtter ikke dette enda såvidt meg bekjent).
BDD er mye mer enn det jeg har vist her. For en praktisk introduksjon til et annet BDD rammeverk som heter MSpec anbefaler jeg at du tar en titt på Rob Conery's screencast om BDD - meget inspirerende. Og for mer informasjon om BDD generelt er behaviour-driven.org en bra ressurs. Det har vært mye forvirring rundt testdreven utvikling, og BDD gir en litt annen vinkling på problemet, og et sterkere fokus på adferd, noe som vil kunne øke kvaliteten på programvaren du produserer.
Knagger: BDD, Contiki, Dan North, TDD