Schaken en Unit Tests
Geplaatst door Scato Eggen op 21 May 2010
Tag(s): Webapplicaties, Hobbyprojecten, Artikel
Als er een nieuwe trend is ben ik de laatste die erop duikt. In het licht van een nieuw hobbyproject heb ik dus maar eens twee oude hypes uit de kast getrokken: Web API's en Unit Testing.
Het Plan
Spelletjes programmeren is altijd leuk. Tenminste, dat vind ik. Spelregels kosten meestal heel wat tijd en zijn lastig te testen. Unit tests zouden hiervoor dus ideaal zijn. Het schrijven van een spelletje is dus een mooie gelegenheid om te leren werken met unit tests.
Helaas ben ik zelf niet heel goed in het ontwerpen van gelikte interfaces, dat laat ik aan anderen over. Door een applicatie te splitsen in een client en een server kan ik me mooi bezig houden met de spelregels. Zend Framework heeft een interface voor Web Services waarmee je makkelijk een Soap, JSON of AMF Service kan maken.
Dus ik dacht: waarom niet alle drie? Ik schrijf een web service in PHP. Ik zorg dat deze werkt door middel van unit tests. Ik bied deze web service aan en iemand anders schrijft hier een client voor. Vervolgens kunnen programmeurs ieder hun eigen versie van hetzelfde spelletje schrijven. De ene speler speelt een Flash-versie en de andere een Java-versie... tegen elkaar!
Schaken
Zoals gezegd is het programmeren van een spelletje al lastig genoeg. In plaats van zelf een spel te bedenken heb ik daarom een bestaand spelletje genomen waarvan ik me niet kan voorstellen dat er copyright op ligt: schaken.
Eerst heb ik een ruw ontwerp gemaakt. Je hebt een spel, een bord, twee spelers, de vakjes en de stukken. Vervolgens heb je acties (zoals opgeven) en zetten (waaronder ook rokeren en en passant slaan). Van deze klassen heb ik de meest voor de hand liggende methodedefinities uitgewerkt. Ik heb echter expres nog geen enkele methode ingevuld, onder de noemer test driven design.
Triviale Tests
De volgende stap in test driven development is om alle unit tests te schrijven. Maar wat test je dan? Om het mezelf niet al te moeilijk te maken ben ik nog niet aan de slag gegaan met het testen van de web service zelf. Ik heb dus alleen tests geschreven om te kijken of alle domeinobjecten zich gedragen zoals dat hoort. Een voorbeeld:
public function testGetFile() {
$file = $this->_getRandomFile();
$rank = $this->_getRandomRank();
$square = new OpenChess_Model_Square($this->_board, $file, $rank);
$this->assertEquals($file, $square->getFile());
}
Dit ziet er redelijk triviaal uit, maar het werd me al snel duidelijk waarom het toch handig is om ook getters en setters te testen. Ten eerste doorloop je hiermee de code elke keer dat je je test uitvoert. Dat is vooral handig bij PHP, dan weet je namelijk zeker dat er geen enkele parse error in je code is geslopen. Ten tweede moet je onthouden dat ik nog geen enkele methode had uitgewerkt. Bij het uitwerken van die methodes zit heel veel dom copy-paste-werk. Makkelijk om een foutje mee te maken. Fijn dus dat je het niet allemaal hoeft na te lezen. Bovendien kan een ander stuk van je code rare dingen doen door een typfout in een setter. Dan is het fijn om automatisch gewaarschuwd te worden dat zo'n setter het niet doet.
Test-Driven Design
En zo was ik al snel 5 uur bezig met het schrijven van code zonder ook maar enige spelregel te implementeren. Ik was alleen nog maar spelregels aan het testen en geen enkele test slaagde. Dat voelt een beetje als droogzwemmen. Interessant werd het pas toen ik de verschillende zetten moest gaan testen. Het idee was dat de web service per speelstuk alle geoorloofde zetten meestuurt naar de client. Die vraag je natuurlijk op met een enkele methode. Wat echt een hels karwij wordt. Bovendien, wat moet je dan testen? Moet je per type speelstuk nagaan of alle zetten kloppen? Dan ben je een eeuwigheid bezig met allemaal stukken op het bord zetten en alle zetten tellen.

Bovendien zit er overlap tussen wat verschillende type speelstukken kunnen zetten. Zo kunnen een toren en een loper samen wat een koningin in haar eentje kan. Bovendien moet het weinig uitmaken welke van de vier richtingen je test. Je kunt veel beter een van de richtingen testen en je code zo schrijven dat je de richting zelf opgeeft. (Ik moest gelijk denken aan een constructie met delta-x en delta-y. Linksboven is dan -1, +1.)
De oplossing is dan ook zo eenvoudig dat het wel een goede oplossing moet zijn: je maakt een aparte klasse (MoveHelper) met methodes voor rechtlijnige bewegingen (-1, +1 of -2, +2), enkele stappen (+2, +1 voor de paardensprong), al die rare stappen voor pionnen en natuurlijk het rokeren. Vervolgens test je elke methode apart. Je zou dan daar bovenop de bewegingen van alle typen stukken nog een keer kunnen testen, maar dat is eigenlijk niet eens meer nodig. Die methodes worden namelijk kinderlijk simpel (en dus weinig foutgevoelig).
public function getValidMoves() {
$moveHelper = new OpenChess_Model_MoveHelper($this);
$moveHelper->addSingleMove(-2, -1);
$moveHelper->addSingleMove(-2, 1);
$moveHelper->addSingleMove(-1, -2);
$moveHelper->addSingleMove(-1, 2);
$moveHelper->addSingleMove(1, -2);
$moveHelper->addSingleMove(1, 2);
$moveHelper->addSingleMove(2, -1);
$moveHelper->addSingleMove(2, 1);
return $moveHelper->getMoves();
}
En tot slot de unittest die erbij hoort:
public function testSingleMove() {
$piece = $this->_placeWhiteQueenAt('d', 4);
$df = rand(-1, 1);
$dr = rand(-1, 1);
while($df === 0 && $dr === 0) {
$df = rand(-1, 1);
$dr = rand(-1, 1);
}
$targetFile = chr(ord('d') + $df);
$targetRank = 4 + $dr;
$moveHelper = new OpenChess_Model_MoveHelper($piece);
$moveHelper->addSingleMove($df, $dr);
$moves = $moveHelper->getMoves();
$this->_assertOneMove($moves);
$this->_assertMoveTo($targetFile, $targetRank, $moves[0]);
$this->_assertMoveType(OpenChess_Model_Move::TYPE_PLAIN, $moves[0]);
$this->_placeBlackQueenAt($targetFile, $targetRank);
$moveHelper = new OpenChess_Model_MoveHelper($piece);
$moveHelper->addSingleMove($df, $dr);
$moves = $moveHelper->getMoves();
$this->_assertOneMove($moves);
$this->_assertMoveTo($targetFile, $targetRank, $moves[0]);
$this->_assertMoveType(OpenChess_Model_Move::TYPE_CAPTURING, $moves[0]);
}
Conclusie
Ik ben uiteindelijk meer tijd kwijt geweest aan het schrijven van de unittests dan aan de code zelf. Ik ben echter bijna geen tijd kwijt geweest aan het testen van de spelregels. In het begin kwam ik erachter dat ik mijn eigen stukken kon slaan. Afgezien daarvan heb ik geen fouten kunnen ontdekken.
Daarnaast moet ik zeggen dat de code die het uiteindelijk opleverde extreem overzichtelijk was. Wellicht komt dat ook wel een beetje doordat ik moeilijk van gedachte kon veranderen: dan moet je namelijk en de code zelf en de unit tests aanpassen. Twee keer zoveel werk, dan laat ik het wel zoals het is. Het werkt.
