Tools4ever Tech Blog

Code voorbeelden test-driven development

Ralph Langendam - Sr. Software Engineer | 13 september 2016

In de vorige blog hebben we gekeken naar hoe Test-Driven Development (TDD) een software ontwikkelproces kan aanvullen c.q. verbeteren met een impliciete focus op kwaliteit. Deze keer werk ik een kata in C++ uit om het voorgaande te concretiseren en met voorbeelden te larderen. Wanneer TDD met codevoorbeelden gestalte krijgt komt het nog meer tot leven.

Bootstrapping

We beginnen met het bootstrappen van een test framework. Van de vele test frameworks die ik gezien heb ben ik in het bijzonder gecharmeerd geraakt door de eenvoud van Catch: een header-only C++ testing framework dat ook een BDD-stijl ondersteunt. Door de eenvoudige setup is Catch ideaal voor beginners.

Een werkend eindresultaat van deze bootstrapping is te vinden als de "Initial commit" van deze GitHub repository.

Download catch.hpp en kies een van de volgende opties:

  • Include catch.hpp bij het bestaande main entry-point en delegeer de controle naar het test-framework.
  • Maak een nieuw cpp-bestand en laat Catch het main entrypoint genereren. Link het output object tegen een gemeenschappelijke statische library zonder main entrypoint.

In beide gevallen moeten de testers zelf statisch tegen bovenstaande translation unit aan gelinkt worden om de automatische test registratie (tijdens de elaboratiefase) te laten werken. Het uitvoeren van de test applicatie is nu voldoende om alle testers uit te voeren. Zie ook deze Tutorial voor meer informatie.

Tijd voor de eerste test. Maak een nieuw bestand test.cpp en link deze statisch tegen de applicatie.

Uitvoeren geeft nu:


FizzBuzz

We zullen nu FizzBuzz als kata uitwerken. FizzBuzz is een programma dat de nummers 1 t/m 100 op het scherm print. Echter, alle veelvouden van 3 zijn vervangen door de tekst "Fizz" en alle veelvouden van 5 door "Buzz". Voor veelvouden van zowel 3 als 5 wordt "FizzBuzz" geprint.

We beginnen met een nieuw bestand FizzBuzzTester.cpp. Op de IO na kan alle functionaliteit in een unittest worden ondergebracht.

Compilatie is de eerste stap in het automatisch testen. Met de huidige input faalt de compilatie en daarmee dus de test. Immers, er is noch een bestand met de naam FizzBuzz.hpp, noch een functie FizzBuzz die een output iterator accepteert. Zonder C++ concepten is de meest eenvoudige oplossing het toevoegen van het missende bestand met een generieke functie.

De applicatie bouwt en de runtime testen slagen. Er is in dit stadium nog niet veel te refactoren, dus we kunnen beginnen met de volgende TDD-cyclus.

Onze eerste falende runtime test:

De oplossing is simpel:

Ofschoon de test nu slaagt, worden zowel de waarde 100 als het type std::string gedeeld. Merk hierbij op dat 100 dezelfde betekenis draagt als in de testen. We verwachten immers 100 strings. Deze waarde refactoren we dan ook naar een constante. std::string in de tester daarentegen, geeft een eis op het resultaat type. Dit hoeft echter niet hetzelfde type te zijn als waar vanaf impliciet geconverteerd wordt in FizzBuzz. We hadden std::string in FizzBuzz immers ook mogen vervangen door char const*, ware het niet dat we C++ gebruiken...

Tijd voor de volgende TDD-cyclus.

Nu de kapotte test repareren:

Het refactoren van de string literal ‘voelt’ hier verkeerd. Immers, de test vraagt alleen om gestelde string output, terwijl FizzBuzz de literal “1” gebruikt als afkorting voor std::to_string(1). Gezien dit semantisch verschil, beoordelen we refactoring van “1” ongeoorloofd. De volgende cyclus dan maar.

De eenvoudigste oplossing is om de lambda-expressie stateful te maken.

De tester is nog steeds alleen geïnteresseerd in de string output, terwijl FizzBuzz zelf nu impliciet het gebruik van de std::to_string functie dupliceert in de afkortingen “1” en “2”. Dat niet alleen, maar ook het feit dat de state in deze het verschil tussen 1 en 1+1 vertegenwoordigt, betekent dat we de vrijheid in alle outputs op indices groter dan 1 kunnen gebruiken om een counter de rol van state te laten vervullen.

Het is nu evident dat de 1 in FizzBuzz en “1” in de tester een andere betekenis hebben en geen refactoring behoeven. Merk op dat wanneer FizzBuzz volgens de functionele specificatie, bijvoorbeeld bij aanroep, een startwaarde mee zou krijgen, dit een ander verhaal zou zijn geweest. Tijd voor de volgende cyclus.

Deze test repareren we eerst op de makkelijke manier.

Fizz is een van de drie speciale output waarden van FizzBuzz en kunnen we refactoren.

In de volgende cyclus bekijken we:

Echter, deze test slaagt direct en slaan we daarom over.

De simpele oplossing lijkt niet veel schoner te worden.

Wederom kunnen we “Buzz” als speciale output waarde refactoren.

Voor de volgende TDD-cyclus kijken we naar de zesde waarde.

Eerst maar eens simpel oplossen met nog een cyclomatische complexiteit verhogende conditie.

Zoals de functionele specificatie voorschreef zijn zowel 3 als 6 veelvouden van 3. Dit kunnen we ook eenvoudiger detecteren door te controleren op rest na deling.

De cases voor 7, 8 en 9 gaan reeds goed, dus hoeven we ook niet toe te voegen.

Dit is bekend terrein. Laten we direct de uiteindelijke oplossing toevoegen.

Het is nu pas bij 15 dat we onze eerstvolgende afwijking van de specificatie tegenkomen.

Gelukkig is de makkelijkste oplossing eenvoudig toe te voegen.

Refactoring daarentegen lijkt nu iets lastiger, maar gelukkig hebben we alle voorgaande test cases nog om eventuele fouten op te sporen. Merk op dat 15 = 3 * 5, zodat 15 % 3 = 0 en 15 % 5 = 0.

Merk op dat de else statements zijn verwijderd en we buzz appenden bij een fall-through. Dit was onze laatste TDD-cyclus, omdat we geen falende testen meer toe kunnen voegen. Door het opvatten van 15 als veelvoud van zowel 3 als vijf hebben we immers ook het correcte gedrag voor alle veelvouden van 15 verkregen.

De FizzBuzz kata is nu bijna ten einde. We moeten alleen nog een applicatie met de juiste output opleveren. IO valt buiten de test en concentreren we dan ook louter in main.

We zijn nu klaar. De FizzBuzz kata is in baby steps doorlopen. Zowel het eindresultaat als alle tussenliggende stappen zijn ook te vinden op GitHub.

In de praktijk zal je als ontwikkelaar al snel grotere stappen nemen dan zojuist geïllustreerd. Dit voorbeeld is dan ook bedoeld om het potentiële minimalisme te tonen, waarop binnen TDD kan worden teruggevallen. Het uit het blote hoofd verzinnen van een implementatie kan soms best lastig zijn. Het in één keer opschrijven van de uiteindelijke FizzBuzz implementatie is dan ook niet voor iedereen intuïtief. TDD vormt mede hiervoor een waardevol hulpmiddel om code van bovengemiddelde kwaliteit op te leveren. De code is bovendien optimaal herbruikbaar, omdat deze reeds in isolatie getest wordt.