TEST 123
Na de eerste twee delen ‘Clean your code met fat arrows’ en ‘De S van SOLID’, wil ik het drieluik over kwaliteit in code besluiten met het onderwerp ‘testen’.
En dat is wel vaker het geval; testen als sluitstuk als het gaat om kwaliteit. We zitten met testen een beetje in ons maag; moeten we alles testen, eerst testen dan coderen, testen kost te veel tijd, alles moet onder test en zo kan ik nog wel een tijdje doorgaan.
Laten we eens wat zaken rondom testen onder de loep nemen.
Als ik het heb over testen, dan bedoel ik daarmee ‘unit testing’, ‘feature testing’ of het meer algemene begrip ‘automated testing’: Tests die geschreven worden met behulp van een bepaald framework en die vanuit een proces automatisch gestart kunnen worden.
Unit Testing
Bij unit tests gaat het om het testen van kleine stukjes code (units). Vaak wordt een functie als unit genomen. Belangrijk is dat alleen de functie wordt getest en niet zaken waar de functie weer vanaf hangt (dependencies). Om alleen de functie te kunnen testen, worden de dependencies ‘gemocked’. Dit betekent dat er (extra) code wordt geschreven die zich voordoet als de echte code. Op die manier is precies dat te testen wat je daadwerkelijk wilt testen.
Ook belangrijk is dat je per test maar één bewering (assertion) test.
Stel je hebt een Guitar object.
Class Guitar {
public $brand;
public $type;
public function setProperties($brand, $type) {
$this->brand = $brand;
$this->type = $type;
}
}
Er is een functie setProperties($brand, $type). Je moet het merk en type doorgeven om een gitaar te kunnen maken. Om deze functie goed te testen moet je een test schrijven die de brand test en een aparte test die het type test. Dat is eigenlijk raar, omdat het dezelfde functie is. Beter is het om de functie te splitsen in een setBrand($brand) en een setType($type). Op deze (heel eenvoudige) manier leidt het schrijven van testen tot beter code design. De nieuwe functies voldoen wel aan het principe van ‘Single Responsibility’ (de S van Solid).
Test Coverage
Test coverage zegt iets over hoeveel code er getest wordt. Wordt iedere functie getest (100%) of maar een klein gedeelte van de code. Test coverage is goed te meten en dat maakt het een populair getal. Zo populair zelfs dat Test coverage er voor zorgt dat er meer getest wordt om het testen, dan dat er daadwerkelijk wordt getest op de juiste beweringen. Zonder een goede assertion is een test niets waard.
Daarbij komt dat een applicatie vaak een core heeft. Code die vaak wordt uitgevoerd, waar vaak veranderingen op zijn en waar (in het verleden) vaak bugs op waren. Het is veel belangrijker om de core goed onder test te hebben, dan een 100% coverage na te streven.
Test Driven Development (TDD)
Bij TDD begin je eerst met het schrijven van een test, gebaseerd op een requirement. Daarna schrijf je de code, die de test uiteraard doet slagen.
Bij TDD test je dus niet anders, je trekt het alleen helemaal naar voren in je development workflow. Hoe eerder je een test schrijft hoe beter. Wat is er dan beter dan een test schrijven voordat je nog maar een regel code hebt geschreven.
Vaak schrijf je de test op basis van een requirement. Om een goede tests te schrijven, moeten de requirements ook duidelijk zijn.
Veel developers zeggen dat als je de test zo vroeg schrijft, je hem ook zo vaak moet aanpassen. Maar dat is niet waar. Een requirement waar de test op gebaseerd is of zou moeten zijn, verandert vaak helemaal niet. Het is het begrip van dit requirement dat veranderd.
Use the source, Luke
Het grootste bezwaar tegen TDD of testen in het algemeen, is dat het veel tijd kost. Niet alleen het schrijven van de tests, maar ook het steeds aan moeten passen van de tests wanneer de code wordt gewijzigd . Ook wordt vaak aangegeven dat men het moeilijk vindt om de test te schrijven, omdat er bijvoorbeeld veel code moet worden gemocked.
Het antwoord ligt eigenlijk in de basisgedachte van TDD.
TDD gives us feedback on the quality of our design.
If a test is difficult to write, our design is poor.
Het feit dat tests moeten worden herschreven omdat er code is gewijzigd, geeft aan dat de tests verkeerd waren in eerste instantie. Tests moeten het gedrag van code testen en niet de interne werking. En het gedrag van een functie/object verandert niet daadwerkelijk over tijd.
Wanneer code wordt geschreven met de S van SOLID in het achterhoofd is deze ook veel eenvoudiger te testen, omdat de functie maar precies een enkel ding doet. Goede, nette code is goed testbaar.
Daarbij komt dat een mooie suite aan tests goede documentatie vormt voor nieuwe programmeurs op het project.
$this->assertTrue($shouldDo)
Het is goed om testen op te nemen in je workflow. Het test niet alleen je code, maar geeft een indicatie over het design van je code. Daarbij is het goede documentatie voor andere programmeurs. Probeer de output en gedrag van de code te testen en niet de implementatie. Goede tests moeten blijven werken, ook na een refactoring.
En test vooral niet om het testen.
“Legacy code is code without tests. — Michael Feathers”