Clean Code - Hoofdstuk 9 en 10

Ingediend door Dirk Hornstra op 11-may-2019 23:34

Eind november 2018 hebben we bij het backend-overleg hoofdstuk 1 en 2 doorgenomen (link), in januari 2019 hoofdstuk 3 en 4 (link), in maart 2019 hoofdstuk 5 en 6 (link) en in april hoofdstuk 7 en 8 (link). Omdat mijn agenda voor de komende weken al aardig volgepland is lijkt het me verstandig om deze zaterdagavond alvast hoofdstuk 9 en 10 door te nemen.

Hoofdstuk 9.
Dit hoofdstuk gaat over Unit-Testing.

In het verleden werd er niet getest of de test bestond uit een simpele controle en de testcode werd weggegooid. We zitten inmiddels in de tijd van Agile en Time Driven Development. Een beweging die programmeurs stimuleert om geautomatiseerde unit-testen te maken. De 3 wetten van Time Driven Development zijn:

  1. Je mag geen productie-code maken voor je een falende unit-test gemaakt hebt.
  2. Je mag niet meer code in je unit-test schrijven dan voldoende is om een case te laten falen. Niet-compilen staat gelijk aan falen.
  3. Je mag niet meer productie-code schrijven dan voldoende is om te huidige falende test succesvol te kunnen doorstaan.

 

Met deze regels wordt productiecode gelijktijdig geschreven met de testcode (eerst de testcode, dan productiecode). Maar dit is niet de manier. De hoeveelheid testcode wordt groter dan de productiecode en kan waarschijnlijk niet meer beheerd worden.

Houd testcode schoon

We krijgen een voorbeeld van een team wat niet nette testcode maakte, quick-and-dirty. Bij wijzigingen in de productiecode moet ook die verweven testcode bijgewerkt worden. De tijdsinschattingen van wijzigingen nemen toe omdat de testcode een blokkerende factor begint te worden. Vervolgens wordt besloten de testcode te laten vallen. Vervolgens durft bijna niemand meer code op te schonen of zaken aan te passen omdat niet meer getest kan worden of nog de gewenste/juiste antwoorden uit het systeem rollen.

Test maken de -ilities mogelijk

Heb je geen tests, dan kan elke wijziging een bug introduceren. Met een goede testsuite kun je met een gerust(er) hart die wijzigingen doorvoeren.

Clean Tests

Wat is "schone test-code"? We krijgen een voorbeeld van één grote lap code waarin allemaal checks gedaan worden. De auteur refactort dit naar een build-operate-check-pattern. Eerst wordt de structuur opgebouwd, daarna acties uitgevoerd en als laatste de checks uitgevoerd. 

Domain-Specific Testing Language

In het voorbeeld worden verschillende API-calls uitgevoerd / systeemfuncties. Door die binnen een eigen functie met een duidelijke naam te zetten is in één blik te zien wat de bedoeling van de test is.

A Dual Standard

Hoewel je testcode clean moet houden, hoef je testcode niet super te optimaliseren. Werk je met de productiecode in een omgeving waar je een gelimiteerde hoeveelheid geheugen beschikbaar hebt, dan zul je daar tweaks voor doorvoeren. Maar de testcode zal op een andere plek uitgevoerd worden, die beperking niet hebben en dus niet geoptimaliseerd hoeven worden. In een productie-omgeving zou je voor een stuk tekst een StringBuilder gebruiken, in je testcode is het aan elkaar plakken van strings misschien wel "goed genoeg".

One Assert per Test

Als je een testfunctie uitvoert, moet er in die functie aan het eind één true of false resultaat zijn. Zijn het er meer, dan moet je ze splitsen naar losse test-functies. 

Single Concept per Test

In het voorbeeld wordt een datum gegeven. Daar worden herhalende controles op uitgevoerd. Totaal onoverzichtelijk wat nu het doel van die test is. Uiteindelijk blijkt dat er checks zijn op "tel 30 dagen bij laatste datum van een maand en controleer dat die datum binnen de volgende maand valt", maar vervolgens wordt dat op de 30e en op de 31e gedaan en check je meerdere concepten (als de laatste dag de 30e is dan ..., als de laatste dag de 31e is dan...). En vervolgens is de situatie van de maand februari (28/29) "vergeten". Dat zit nu allemaal in één test-functie. Hier zou je 4 losse functies van kunnen maken zodat ze per stuk "1 concept" hebben.

F.I.R.S.T.

Clean test-code volgt deze afkorting, we hebben dus een ezelsbruggetje!

Fast, testen moeten snel zijn. Als je lang moet wachten worden testen minder vaak uitgevoerd en zal dat weer meer werk opleveren.

Idependent. Als je tests moeten los van elkaar uitgevoerd kunnen worden. De ene testfunctie moet niet afhankelijk zijn van de andere, want als er dan één breekt, dan breekt de rest ook.

Repeatable. Je tests moeten tot het einde der tijden herhaald kunnen worden, zowel op productie, op test en op je lokale dev-omgeving.

Self-validating. Een test heeft een boolean output en zegt dus of de boel gelukt is of mislukt. Geen handmatige controles in logbestanden of zelf nog zaken met elkaar vergelijken.

Timely. Schrijf je testcode voor de productiecode. Als je het na de tijd doet zul je erachter komen dat code vaak niet gebouwd is om getest te worden en zul je (te?) veel moeten ombouwen.

 

Persoonlijke noot: in het verleden wilde ik nog wel eens een console-applicatie maken om bepaalde zaken te testen. Want ik had nog nooit een test-programma aan een project toegevoegd. Tot ik een import moest controleren. Testproject aangemaakt, testfunctie aangemaakt en zo op een snelle manier de fout kunnen opsporen. En nu dus een functie beschikbaar die ik in de toekomst nogmaals kan gebruiken voor controles. Die console-applicatie ga ik niet meer gebruiken, ik ga dit nu vaker doen!

 

Hoofdstuk 10.
Dit hoofdstuk gaat over classes.

Organisatie

Een class wordt opgebouwd uit public static constants, private static variabelen, private instance variabelen, public functions, private functions na de public functie die er gebruik van maakt. 

Classes moeten klein zijn

Eigenlijk hetzelfde verhaal als bij functies, hou de boel zo kort/klein mogelijk. Bij functies ging het echt om het aantal regels, bij classes gaat het om de verantwoordelijkheden van een class (responsibilities). Als jouw class het aanmaken van schermen regelt, de volledige databaseconnecties afhandelt en ook nog de slack-integratie zelf implementeert, dan heeft je class veel-te-veel verantwoordelijkheden. Je moet kunnen omschrijven wat een class doet, komt daarin "de class doet dit.. en dit", dan is die "en dit" iets voor een nieuwe class. De naam moet ook aangeven wat de verantwoordelijkheid is, dus geen termen als "Manager", "Processor" e.d. gebruiken.

The Single Responsibility Principle

Dit principe geeft aan dat er maximaal één reden mag zijn om een class te wijzigen. Als jouw class een externe API afhandelt en die API wijzigt, dan moet jouw class aangepast worden (is verantwoordelijk voor het afhandelen van die API), dan is dat goed. Als jouw class aangepast moet worden omdat de resultaten niet meer opgeslagen worden in tekstbestanden, maar in een database dan is dat niet goed, dan heeft jouw class dus ook de verantwoordelijkheid voor de lokale opslag. Dat zou een aparte class moeten doen!

Met dit principe zul je dus redelijk veel classes krijgen, maar het zal wel duidelijk zijn wat een class doet en verantwoordelijk voor is.

Cohesion

Cohesion staat voor de verwevenheid van functies en variabelen in een class. Als deze veel interactie met elkaar hebben, dan is er een hoge cohesie, dat is goed.

Maintaining Cohesion Results in Many Small Classes

Als je functies opsplitst en dat daardoor de cohesie daalt, dat is meestal een teken dat je er een nieuwe kleine class van kunt maken.

Organizing for Change

In het boek wordt een voorbeeld met een SQL-class gegeven. Die class heeft methodes, maar mist nog de Update. Dit zal impact hebben op de class en er zal waarschijnlijk wat omgebouwd moeten worden. Als je dat doet moet het volledig hertest worden. Als er een nieuw statement komt, moet de SQL class aangepast worden. Maar als het select-statement uitgebreid gaat worden, moet ook de SQL class aangepast worden. Een teken dat er niet voldaan wordt aan het Single Responsibility Principe. Tijd om de boel om te bouwen. De class wordt een abstract Class, de items die statements waren worden nu losse classes die de abstracte "base" class uitbreiden met hun eigen functionaliteit. Voldaan aan het SRP principe en nu ook beter te testen.

Isolating for Change

In het voorbeeld wordt data uit een feed gehaald, Omdat die data kan wijzigen, kun je geen test maken. Daarom wordt een interface gemaakt, waar de feed van erft, maar waar nu ook een eigen test-feed met vaste waarden aan doorgegeven kan worden. Op die manier kan er wél getest worden. Het minimaliseren van het koppelen van zaken staat bekend onder de term Dependency Inversion Principle. Dit geeft aan dat classes afhankelijk moeten zijn van abstracties en niet van concrete details.

 

Persoonlijke noot: bij .NET Core is de basis de dependency injection, wat ervoor zorgt dat je code in de basis al een stuk beter te testen is.