Tools4ever Tech Blog

Test-Driven Development in de praktijk

Ralph Langendam - Sr. Software Engineer | 6 september 2016

In mijn jaren als professioneel softwareontwikkelaar ben ik met veel bedrijfsmatig ontwikkelde broncode in aanraking gekomen. Dit is vaak verreweg van een plezierige ervaring geweest. Met inzicht vanuit een theoretische achtergrond is het duidelijk dat er gekozen wordt voor snelle oplossingen die op korte termijn veel business value toevoegt. Hierin herkennen we het Pareto principe, ook wel de 80-20 regel genoemd, in combinatie met gebrek aan kwaliteit binnen de project management driehoek (ook wel duivelsdriehoek).

De gevolgen hiervan zullen menigeen bekend zijn. Nieuwe projecten kennen een hoog ontwikkeltempo wat steeds verder afneemt naarmate het project vordert. Zombie ScrumHet opbouwen van technical debt keert zich al snel tegen de wens om nieuwe features uit de ontwikkeling te blijven stampen. De beruchte 20% loopt hierdoor flink in de papieren. Deadlines worden niet meer gehaald en de frustratie van ontwikkelaars leidt tot micromanagement van de teamleider. Het strakker sturen op het proces biedt echter geen soelaas. Ook Agile ontwikkelteams zijn gevoelig te verworden tot Scrum zombies. Over dit onderwerp is er op 28 september 2016 een Developer Tech Nite bijeenkomst.

Focus en kwaliteit

"Hoe dit tij te keren?" is de vraag. Het antwoord is veelzijdig en de oplossing niet eenvoudig. Echter, de lange termijn oplossing draait mede om een kern van focus op kwaliteit. Er lijkt vaak, binnen lopende projecten, nauwelijks mogelijkheid om op kwaliteit te concentreren. Het kan daarom helpen als kwaliteit een onderdeel is van het ontwikkelproces zelf. Hierbij moet de inrichting dusdanig zijn dat ook de kwaliteitswinst, op korte termijn, bijdraagt aan de efficiëntie van ontwikkeling. Op deze wijze gaat de toename van kwaliteit niet of nauwelijks ten koste van de ontwikkeltijd.

De Duivelsdriehoek en het Pareto principe zullen echter in het nadeel blijven werken, maar mijn ervaring is dat deze last een stuk dragelijker wordt indien de kar getrokken wordt door goede softwareontwikkelaars die met liefde voor het vakmanschap op pragmatische wijze streven naar kwaliteit en schoonheid in hun oplossingen. Mensen die niet bang zijn om nieuwe dingen te leren; graag kennis delen met collega's en de capaciteit hebben om het overzicht in complexe projecten te behouden. Het zijn deze meest competente lieden die zich van de introverte code kloppers onderscheiden en floreren in een omgeving waarin zij de ruimte krijgen zich te ontplooien.

Snelheid versus kwaliteit

Software programmeren vereist veelal slechts kennis, denkt men. Maar software ontwikkelen is veel meer. Het vereist ook intelligentie en abstract denkvermogen. Er is vooralsnog geen enkele procedure die creativiteit of intelligentie kan vervangen. Dit weerspiegelt ook het Agile Manifesto: "Individuals and interactions over processes and tools". Ik kan dan ook geen recept geven om een succesvol softwareproject uit te voeren. Ik kan slechts schrijven over mijn ervaring binnen het continue spanningsveld tussen snelheid en kwaliteit in het bedrijfsleven.

Ons vak is nog niet zo oud, maar de rappe ontwikkeling van technologieën leidt ertoe dat er al snel significante legacy wordt opgebouwd. Het behouden van compatibiliteit en het onderhouden van legacy code is een uitdaging op zich. Voordat we met iets nieuws kunnen beginnen, zonder de bestaande problemen te verergeren, is het belangrijk met zo min mogelijk terughoudendheid aan legacy broncode te kunnen werken. Het niet durven aanpassen van bestaande software is gedreven door angst en leidt altijd tot een overbodige toename van complexiteit. Angst is ook hier een slechte raadgever.

Neem dus de tijd om bestaande broncode te doorgronden in de wetenschap dat zelfs een oppervlakkig tot matig begrip van een legacy interface, ook op korte termijn, tijdwinst oplevert. Een abstractie laag om legacy code is geen oplossing voor problemen in de code zelf. Er is daarom geen ontkomen aan het bestuderen van een stukje geschiedenis; niet bepaald het lievelingsvak van veel bèta's. Het automatisch testen, en daarmee fixeren van onbekend gedrag achter zo'n interface, helpt mij vaak enorm om een functioneel beeld van op te bouwen. Veel van de adviezen in het, naar mijn mening voor iedere ontwikkelaar verplichte leesvoer Working Effectively with Legacy Code draaien om dit principe.

De procedure

Test-Driven Development - cyle of lifeIedereen die wel eens van Test-Driven Development (TDD) heeft gehoord weet dat dit proces kan bijdragen aan de kwaliteit van code. De procedure is eenvoudig te formuleren, maar blijkt een uitdaging om correct in de praktijk tot uitvoer te brengen:

  1. Schrijf een falende unit test die een nieuw stuk functionaliteit fixeert.
  2. Schrijf de meest ondoordachte code om de falende test en alle andere testers te doen slagen.
  3. Refactor productiecode.

Binnen TDD worden geen units ontwikkeld zonder tests. Hierdoor is de dekking theoretisch maximaal en van de geteste functionaliteit de werking gegarandeerd. Aangezien de ontwikkelaar zelf de testcode schrijft kunnen sommige denkfouten zich dubbel, in test en code, manifesteren. Daarnaast blijft het mensenwerk om te beoordelen of een component volledig aan de functionele specificatie conformeert. Bekende valkuil is het denken dat hiaten volledig te ondervangen zijn en alleen rekening gehouden hoeft te worden met het Happy path.

Test Driven Development draagt weliswaar bij aan de kwaliteit, maar geeft er geen garantie voor. Omdat unit testers snel, in isolatie en in willekeurige volgorde moeten kunnen draaien zijn de mogelijkheden voor het testen van globals/singletons, system calls, multithreading en input/output beperkt. TDD behoeft daarom nog aanvulling met (automatische) integratietesten, zowel tussen units onderling als tussen units en voorgenoemde externe factoren. Veel integratietesten kunnen ook op TDD-wijze worden aangevlogen. Sterker nog, TDD helpt juist bij het formuleren van een bruikbare en solide architectuur tussen units onderling.

Speciale aandacht verdienen de vetgedrukte woorden hierboven in het TDD-stappenplan: "falende", "meest ondoordachte" en "refactor".

  • Als een nieuwe test niet faalt is diens bestaan ook niet gelegitimeerd.
  • Waak voor de verleiding om te refactoren nog voordat alle testen slagen. Offer correctheid en schoonheid op om zo snel mogelijk tot een green bar te komen.
  • Elimineer alle duplicatie in de productiecode. Wees genadeloos. Over refactoring alleen al zijn boeken vol geschreven. Persoonlijk vind ik het gelijknamige boek van Martin Fowler heel waardevol.
  • Het refactoren van de testcode zou per definitie niet nodig moeten zijn. Iedere test introduceert immers nieuwe functionaliteit. Refactoring naar parametrisatie van testen geeft aan dat de geteste interface eenvoudiger kan. Oftewel, een deel van de testcode kan naar productiecode verhuizen.
  • De hand van de meester laat zich herkennen in het gebruik van Design Patterns. In combinatie met juiste toepassing van de kennis uit Refactoring to Patterns stijgt de herbruikbaarheid van software aanzienlijk.

Een goede architectuur

Het is moeilijk om uit een functionele specificatie (voor zover aanwezig) een architectuur te ontwerpen. Het kan verstandig zijn om wat langer over een wenselijke architectuur na te denken. Het is vaak beter om de architectuur te laten groeien uit de verwoording van de functionele specificatie in termen van testen. Het probleem is hiermee nog steeds niet eenvoudiger, maar het helpt vaak wel om over een interface na te denken in termen van het wenselijk gebruik. Testers fixeren en valideren dit gebruik in een keer. Er is echter geen vervanging voor kennis, ervaring, intelligentie en creativiteit; ook bij TDD niet.

Het schrijven van testcode kost tijd. Dit zal niemand ontkennen. Echter, zoals al eerder opgemerkt: kwaliteit moet een onderdeel van het ontwikkelingsproces zijn; idealiter zonder dat dit ten koste gaat van de efficiëntie. Mijn claim is dat competentie in het nauwkeurig uitvoeren van TDD bespaart op debugging tijd. Hiermee doel ik op de tijd besteed door ontwikkelaars in debug sessies en/of het schrijven van print statements onder het 'genot' van het regelmatig herstarten en bouwen van de applicatie. Het contentieus uitvoeren van TDD binnen een project zorgt ervoor dat ik zelden de behoefte voel een debugger te gebruiken.

Automatische unit testers vereisen 'slechts' het regelmatig bouwen van de applicatie; aangenomen dat de testers bliksemsnel kunnen worden uitgevoerd. Zelfs wanneer de automatische testers nog niet in een continuous integration omgeving meedraaien is hun toegevoegde waarde, uitgedrukt in kwaliteit, reproduceerbaarheid en vertrouwen, in de praktijk aanzienlijk.

Korte TDD-cycli zijn beter. Het is daarom essentieel om de bouwtijd zo veel mogelijk te verkorten. Hiertoe zijn twee factoren van belang. Door sterk modulair te ontwikkelen en de afhankelijkheden te minimaliseren hoeft er minder herbouwd te worden als er slechts enkele bronbestanden zijn aangepast. Zie ook deze vraag op StackOverflow voor C++ specifieke tips. In mindere mate zijn deze oplossingen ook van toepassing op andere programmeertalen.

Helder geformuleerde testen zijn een vorm van levende documentatie. Iedere vorm van handmatige documentatie is reeds verouderd en waarschijnlijk inconsistent nog voordat de laatste letter is gecommit. Een functionele specificatie vormt de benodigde aanvullende leidraad, op een hoger niveau, waarvan de automatische testen gezamenlijk een representatie vormen. Automatische integratie testen kunnen hier een welkome aanvulling op zijn, zeker wanneer deze in een Behavior-Driven Development (BDD) stijl zijn geformuleerd.

De waarde van testen

Testen schrijven is een integraal onderdeel van softwareontwikkeling. Geen enkele, zichzelf respecterende, softwareontwikkelaar zal de waarde van het (automatisch) testen van software ontkennen. Wanneer testen achteraf moeten worden geschreven voelt dit vaak als een straf. "Het programma werkt al, dus waarom zou ik nog de moeite nemen om er testen voor te maken?" Omgekeerd, door het vooraf schrijven van testen voelt de green bar als een beloning. Je boekt continu voortgang.

Hebzucht is een van de zeven zonden. Te grote stappen nemen leidt vaak tot onevenredig veel langere TDD-cycli en doet veel voordelen van het proces teniet. Praktijkervaring is hier de sleutel. Een cyclus zou, zeker in het begin, niet langer dan een paar minuten mogen duren. Het doel van baby steps is om te leren hoe problemen in behapbare porties kunnen worden opgedeeld. Dit betekent niet dat een ervaren TDD'er zich hoeft te laten afremmen door trivialiteiten en overhead, maar deze moet wel de discipline hebben om terug te schakelen als het moeilijk wordt. Zelfbeheersing is dan ook een deugd.

Voor meer informatie over TDD kan ik het prettig leesbare boek Test-Driven Development by example aanraden, geschreven door Kent Beck, de geestelijk vader van het proces. Om praktische bekwaamheid en routine in TDD te ontwikkelen is een dagelijkse kata mijn advies. Overweeg ook eens een pair-programming sessie of coding dojo tussen collega's te organiseren. Mijn ervaring is dat men de voordelen en het plezier van TDD pas gaat inzien tijdens het opdoen van praktijkervaring.

Ik heb in deze blogpost gekeken naar hoe Test Driven Development een software ontwikkelproces kan aanvullen c.q. verbeteren met een impliciete focus op kwaliteit. In de volgende blog ga ik een kata in C++ uitwerken om het voorgaande te concretiseren en met voorbeelden te larderen. Wanneer TDD met codevoorbeelden gestalte krijgt komt het nog meer tot leven.