Gedrag gedreven ontwikkeling met Java en Groovy
En half jaar geleden woonde ik een presentatie over RSPec van Aslak Hellesøy bij op RubyEnRails 2007. RSpec is een framework dat de ontwikkelaar een DSL aanbiedt voor het beschrijven van verwacht gedrag van een stuk code.
Het schrijven van specificaties die gebruikt worden om te bepalen of code doet wat men ervan verwacht, wordt ook wel “Behavior Driven Development” (BDD) genoemd. Persoonlijk vind ik het een erg intuïtieve manier voor het schrijven van tests. Tevens is het zo dat de rapportages die door bijvoorbeeld RSpec worden gegenereerd ook door een niet-techneut gelezen kunnen worden. Hierdoor wordt hetgeen getest wordt inzichtelijker.
Na wat te hebben geëxperimenteerd met RSpec besloot ik op zoek te gaan naar een soortgelijke oplossing voor Java. Er bestaan ondertussen meerdere smaakjes, die ongetwijfeld allemaal hun voor- en nadelen hebben. Het JDave framework zag er op het eerste gezicht wel goed uit. Ik kwam er echter al snel achter dat Java eigenlijk niet dynamisch genoeg is om dit soort tests te schrijven. Een framework als JDave leunt zwaar op het gebruik van inner classes en beschrijvende methode namen, de code ziet er daardoor niet zo fraai uit.
Uiteindelijk belandde ik bij Easyb. Het Easyb framework gebruikt Groovy voor het schrijven van de specificaties. Groovy is een dynamische scripttaal die direct wordt omgezet naar voor de JVM begrijpelijke Bytecode en zit derhalve erg dicht tegen Java aan.
Voorbeeld: Bowling
Het ‘bowling’ voorbeeld op de RSpec-site leek me een mooi voorbeeld om als basis te gebruiken voor een Java variant.
We beginnen met het vastleggen van de regels, ofwel het verwachte gedrag:
- Het is mogelijk een aantal kegels om te gooien. Het aantal kan nooit lager zijn dan nul, en het mag niet hoger zijn dan 10.
- Na het raken van een aantal kegels moet het aantal overblijvende kegels 10 minus, het aantal geraakte kegels zijn.
- Men kan maximaal twee maal werpen, na een worp wordt het aantal resterende worpen verlaagd met 1.
Natuurlijk zijn er nog veel meer regeltjes… maar dit is voor het voorbeeld wel even voldoende.
De test zullen we tegen een interface aan schrijven die er als volgt uit ziet:
package com.finalist.games;public interface Bowling { /** @param numPins The number of pins to hit */ void hit(int numPins); /** @return The number of pins still standing */ int getPinsRemaining(); /** @return The number of throws remaining */ int getThrowsRemaining(); }
Als we nu een JUnit (4) test zouden schrijven zou deze er als volgt uit kunnen zien:
...@Test public void testHit() { // A default bowling game starts with 10 pins, so if we hit 5 of them 5 // pins should still remain bowling.hit(5); assertEquals(5, bowling.getPinsRemaining()); // After the first throw, 1 throw should be left assertEquals(1, bowling.getThrowsRemaining()); bowling.hit(5); // now 0 pins should still remain assertEquals(0, bowling.getPinsRemaining()); // After the second throw, 0 throws should be left assertEquals(0, bowling.getThrowsRemaining()); } @Test(expected = NoThrowsRemainingException.class) public void testExceedThrows() { // Try to throw more then two times for (int i = 0; i < 3; i++) { bowling.hit(1); } } @Test(expected = IllegalArgumentException.class) public void testHisExceedsMaxPins() { bowling.hit(11); } ...
De informatie over hetgeen we testen is verdeeld over de namen van de methodes en het commentaar. Jammer dat de regels commentaar over wat er nu eigenlijk getest word tussen de code staan. Geen niet-techneut die dat ooit gaat zien. Tevens is het een beetje jammer dat het controleren van excepties in de @Test annotatie wordt beschreven terwijl de andere tests wél in de code staan.
Als we het bovenstaande doen met een in Groovy geschreven specificatie gaat dat er als volgt uit zien:
import com.finalist.games.*bowling = new BowlingImpl() it "new game should have 10 pins remaining", { ensureThat(bowling.pinsRemaining, eq(10)) } it "new game should have 2 throws remaining", { ensureThat(bowling.throwsRemaining, eq(2)) } it "remaining pins should be 7 when 3 pins where hit", { bowling.hit(3) ensureThat(bowling.pinsRemaining, eq(7)) } it "remaining pins should be 1 when another 6 pins where hit", { bowling.hit(6) ensureThat(bowling.pinsRemaining, eq(1)) } it "after throwing twice, no throws should be remaining", { ensureThat(bowling.throwsRemaining, eq(0)) } when "no throws remain, and hit is called", { illegalThrow = { bowling.hit(5) } } then "an exception should be thrown when throwing to often", { ensureThrows(RuntimeException.class){ illegalThrow() } }
Zonder diepgaande kennis omtrent Groovy zal een Java-ontwikkelaar de bovenstaande code kunnen lezen. Zo te zien komen de vergelijkingen een stuk dichterbij onze spreektaal, en daardoor dichter bij hetgeen je eigenlijk aan het testen bent. Tevens is de documentatie onderdeel van de code. De oplossing om middels closures (illegalThrow = {}) te testen of een stukje code een exceptie gooit is ook wat eleganter dan de annotatie van JUnit; op deze manier zou je meer tests per methode kunnen uitvoeren.
Het reportage gedeelte van Easyb is nog niet heel geavanceerd. De output is in de vorm van een XML file. Met een eenvoudige XSLT kun je daar natuurlijk mooi gekleurde HTML van maken:
Conclusie
Ik ben erg enthousiast over Behavior Driven Development. Gebruik makende van scripting talen begint het erop te lijken dat deze ontwikkelmethode ook inzetbaar word voor het Java-platform. Eerlijkheidshalve moet ik wel zeggen dat een framework als Easyb nog wel in de kinderschoenen staat, er zijn daardoor jammer genoeg nog geen mooie plugins voor de verschillende IDE’s en build tools. Ik ben erg benieuwd hoe dit zich in de toekomst verder zal gaan ontwikkelen.
—————————————————————————————
Meer weten over Java-specialist Finalist IT Group?





Dit is dus echt iets voor JRuby!
Remco Bos - december 14, 2007 14:33
@Remco Bos
Persoonlijk vind ik het feit dat Groovy dichter bij java staat en de mogelijkheid biedt met statische types te werken een voordeel. Ik zou echter graag een implementatie van JRuby zien die dit doet… tips?
Peter Maas - december 14, 2007 15:01