Mijn visie op software testen

Deze blog is een samenvatting van een kennissessie die ik gehouden had bij Alliander over mijn kijk op software testing.

Disclaimer:
Dit is mijn persoonlijke visie en is gebaseerd op mijn eigen ervaringen bij Alliander.
De manier van testen kan per project, team en bedrijf verschillen en er is geen enkele uniforme manier waarop je hier naar kan kijken. Neem deze visie dus niet aan als absolute waarheid, maar kijk wat hiervoor ook betrekking heeft op je huidige situatie.

Welke tests hebben we bij ons team:

E2E tests:

Doel:
Test dat alle verschillende services correct met elkaar samenwerken

Hoe:
Configureer alle microservices die je binnen de e2e tests wilt laten vallen.
Mock alle externe services buiten deze groep gebruikt worden.
Maak een aantal tests die alle services raken.

Acceptatie tests:

Doel:
Test dat de microservices voldoen aan alle functionele specificaties

Hoe:
Configureer alle microservices die je binnen de functionele tests wilt laten vallen.
Mock alle externe services buiten deze groep gebruikt worden.
Maak voor elke functionele specificatie een test.

Voor elke functionele specificatie hebben we een test in ‘BDD’ style (dus in Gherkin).

Al deze tests zitten in een eigen git repo.

Dit resulteert in een volledige set van functionele tests (de acceptatie tests).

Deze tests zijn de functionele specificaties van de applicatie!

Note:
Bij ons team testen we alle functionele specificaties via de REST interface,
niet via de UI. Die zijn daardoor een stuk sneller en stabieler. Dit zal echter niet bij alle projecten kunnen.

Acceptatie tests vs e2e tests:

Ze lijken erg op elkaar, het enige verschil is de benaming en het doel.

Ere zijn veel meer acceptatie tests dan E2E tests.

Acceptatie tests testen ook de integratie, dus in die zin zou het niet nodig zijn om naast de acceptatie ook nog e2e tests te hebben

Contract testing:

Contract test zijn consumer driver contract tests.

Wat specificeer je hier echter is? Alleen de structuur van de responses of ook de logica wanneer je een bepaalde response terug krijgt? Dat staat vaak niet in de contact test.
Een contract test is volgens ons geen vervanging voor een acceptatie test, omdat daar wel alle logica getest wordt. Als er erg veel microservices zijn dan is een acceptatie test waar alle microservices in zitten echter erg lastig op te zetten en te beheren.
De contract tests hebben we bij ons team niet tussen onze eigen services maar alleen voor services die andere teams van ons gebruiken.

Integratie tests:

Externe services mocken we in de IT test met wiremock mocken, en database met H2.
De integratie tests draai je gewoon met JUnit, en deze draaien dus tijdens de “mvn test”.
Enige verschil met unit tests, is dat de hele app gestart wordt ipv een paar classes.
De tests worden gedraait met gewone build.
De IT test, test de gehele app (start de app , set mock en db op, call met rest)
IT tests kunnen langzaam zijn, hou daar dus rekening mee.
De functie van de IT test: geheel testen zonder externe systemen. Hierin zou je ook alle business requirements kunnen testen. Dat is dan wel dubbel, want die worden ook al in de acceptatie tests getest, maar hierdoor kun je de microservice wel geïsoleerd testen.

Je zou kunnen overwegen om de IT en contract tests te minimaliseren en alle business requirements alleen in de acceptatie tests te doen. Dat doen we on ons team niet omdat we op deze manier een stuk sneller feedback hebben. De business requirements worden dus deel dubbel getest.

Unit tests:

De unit van code hoeft niet perse 1 class te zijn maar vaak is dat wel zo.
Belangrijk is om geen implementatie details te testen!
Hou in de gaten dat de implementatie nog gerefactored moet kunnen worden zonder dat deze test aangepast moet worden.
Bij elke test moet je je afvragen of je de code nog wel kunt refactoren zonder de tests ook aan te moeten passen.

Als er geen logica in een functie zit, dan hoeft deze niet getest te worden, het is niet zinnig om een test te maken voor een functie die zo simpel is dat het risico dat dat fout is te verwaarlozen is!
Kent Beck zegt hier het volgende over:
https://stackoverflow.com/questions/153234/how-deep-are-your-unit-tests/
Kent Beck: “I get paid for code that works, not for tests, so my philosophy is to test as little as possible to reach a given level of confidence”

Test alle edge cases en scenario’s van en algoritme

Je kunt alle depedenties van de unit mocken. Dat zijn “solitary unit tests”.
Je kunt er ook voor kiezen om de dependencies niet te mocken. Dat zijn “sociable unit tests” (zie https://martinfowler.com/bliki/UnitTest.html)
Ik doe zelf beide. Een helper class mock ik bijvoorbeeld niet, maar een httpclient class wel.

Test first (TDD en BDD)

TDD was de eerste methode waarbij je eerst een unit test maakt, en daarna de implementatie.

BDD is doorontwikkeld op TDD en beschrijft een functionaliteit op het nivo van een functionele specificatie.
Dit kun je ook test-first doen.

Als je een logaritme wilt testen, dan kun je dit direct met een unit test doen.
Voor niet-logica (authenticatie bijvoorbeeld) kun je dit het beste doen via een IT of Acceptatie test.

Hoe zit het met de test Piramide?

Er zijn veel varianten van de test piramide.
Hier een verzameling van piramides die op internet te vinden zijn.
Bron: http://www.testingreferences.com/here_be_pyramids.php

Mijn kijk op de test piramide:

De piramide gaat over de hoeveelheid tests

En zijn vaak weinig UI tests in de piramide omdat deze erg langzaam zijn en snel onstabiel worden.

Om dit als vaste regel te hanteren vind ik te ver gaan, ik ben van mening dat je zoveel (of weinig) tests moet maken als mogelijk. De verhouding van het aantal tests tussen unit tests en acceptatie tests kan per project erg verschillen.

Voor een project voor een zelfrijdende auto zouden de verhoudingen wel eens heel anders kunnen zijn dan voor een klanten administratie.

Martin Fowler heeft dit te zeggen over de test pyramide: (https://martinfowler.com/articles/practical-test-pyramid.html):
‘The concept of acceptance tests – proving that your features work correctly for the user – is completely orthogonal to your test pyramid.’
Of te wel: De acceptence tests staan loodrecht tegen de test piramide.

De regels die wij hanteren zijn de volgende:

  • Code waarin logica zit: unit test
  • Koppelingen van 1 microservice met externe systemen: IT test
  • Koppeling tussen 2 microservices van andere teams: Contract test
  • Koppeling tussen alle eigen microservices: E2E tests
  • Alle business requirements: Acceptatie tests

Hierbij zitten onze acceptatie tests in een eigen git repo en testen we de functionaliteit over de verschillende microservices geen.

Daarnaast zijn deze acceptatie tests ook onze functionele specificatie.

Voor welke code schrijf je unit tests:

Met de unit tests testen we kleine stukken logica binnen de code.

Als richtlijn over welke code een unit test nodig heeft en welke niet kunnen we de gedachte overnemen die beschreven staan in deze blog:  https://blog.stevensanderson.com/2009/11/04/selective-unit-testing-costs-and-benefits/

Daarin maakt hij onderscheid in 4 soorten code weergegeven in deze tabel:

Linksboven: Code met logica en weinig dependencies: (voorbeeld: InstallationProfileService)

  • Unit tests op deze code geeft veel voordeel
  • Unit tests zijn makkelijk en duidelijk te schrijven
  • Te herkennen aan tests met veel scenario’s
  • Perfecte fit voor unit test
  • Perfecte fit voor TDD

Linksonderin: Simpele code met weinig dependencies (voorbeeld: ReadyFactory)

  • De code is zo simpel, unit tests geven weinig voordeel
  • De unit tests zijn wel makkelijk te schrijven
  • Je kunt hier unit tests voor maken, of niet, maakt niet zoveel uit

Rechtsondering: Simpele code met veel dependencies (voorbeeld: AccountFlow)

  • Heeft vaak een coördinerende rol
  • Omdat er verder geen logica in de code is, geeft de unit tests weinig voordelen
  • Unit tests zijn wel lastig te maken (veel mocken)
  • Unit tests kun je hier beter niet voor schrijven

Rechtsbovenin: Code met logica en veel dependencies (geen voorbeeld gelukkig)

  • Omdat er logica is, is het noodzakelijk dat er een unit test is
  • Unit tests zijn echter lastig te schrijven, moeilijk te lezen en slecht onderhoudbaar
  • Wellicht is dit te herschrijven naar een component met daarin de logica, en een component met de coördinerende rol

Hierbij is op te merken dat de integratie tests ook alle triviale code raakt. Dus als daar geen unit tests voor gemaakt is en toch niet goed geïmplementeerd is, dan zal dat tijdens het draaien van de integratie tests alsnog wel naar boven komen.