Fout gaat het toch. De doodlopende weg van end-to-end testen.

Roy Braam schreef een interessant artikel in Java Magazine 1/2021 over het twijfelachtige nut van end-to-end testen waar ik het roerend mee eens ben. Om een misverstand meteen uit de weg te ruimen: met een end-to-end test verifieer je niet de integratie van jouw eigen componenten, maar de correcte samenwerking van jouw software met alles waar deze mee interacteert dat niet onder jouw invloedssfeer valt. Dat is van nature onbekend en onbetrouwbaar, dus je zou denken dat je dit uitentreuren moet testen. In de praktijk blijkt echter build for failure de betrouwbaarste strategie om alles wat potentieel mis kan gaan het hoofd te bieden. Laat ik het toelichten aan de hand van een recent praktijkvoorbeeld.

Knole Park, Sevenoaks, Kent, UK

Op onregelmatige tijden (gemiddeld zes keer per jaar) produceert team X een aantal tabgescheiden platte-tekst bestanden op dat door team Y wordt verwerkt in een klassiek extract-transform-load (ETL) proces. Het aantal bestanden en de frequentie van updates wisselt. Aanlevering is in de vorm van een zipbestand dat gedownload kan worden via https en basic authentication (gebruikersnaam en wachtwoord). Het systeem leest dagelijks dit bestand uit en controleert middels een digest hash of de inhoud afwijkt van de laatste verwerking. Zo ja, dan wordt de database bijgewerkt. Meestal is er niets gewijzigd.

Dit is een schoolvoorbeeld van een externe afhankelijkheid waar werkelijk van alles mis kan gaan:

  • De webserver is niet beschikbaar (er komt geen HTTP response terug)
  • De URL is niet bekend (http NOT FOUND error)
  • We mogen er niet (meer) bij (UNAUTHORIZED of FORBIDDEN)
  • Onze request header deugt niet (BAD REQUEST)
  • Het bestand is onleesbaar (geen geldig zipformaat) of leeg.
  • De bestandsnamen in de zip zijn niet zoals verwacht
  • De inhoud van de bestanden is niet correct geformatteerd (afwijkende kolomnamen of -aantallen)
  • De inhoud is correct van formaat maar leidt tot bovenmatige uitval bij verwerking.

Ik zou verder kunnen gaan, maar je ziet meteen de verschillende foutcategorieën en de oplossingsrichtingen. Grofweg hebben we er drie: netwerk, aanlevering (de bestanden) en verwerking (de verwerkingslogica en de database).

Een robuuste foutenafhandeling zorgt er tenminste voor dat de aard van de fout snel en ondubbelzinnig te achterhalen is. Met een makkelijke oplossing die uitgaat van het happy flow scenario betaal je de rekening in de vorm van nietszeggende stacktraces, of erger nog, foutieve data die je klakkeloos hebt geïmporteerd omdat de database er niet over klaagde.

Je zou al deze scenario’s kunnen testen in een productie-gelijke omgeving, maar handig is dat zeker niet. Als je code SOLID is gebouwd kun je met stubs en mocks een heel eind komen. Tests van de netwerkcode is niet geïnteresseerd in de inhoud van het bestand. Voor de validatie van de zipfile hoef je geen bestand te downloaden, maar gebruik je een lokaal testbestand. En voor de integriteitscheck van de csv bestanden heb je weer geen zip nodig.

Of je al die robuustheid ook nodig hebt is een kwestie van schaalbaarheid. In werkelijkheid gaf bovenstaand proces niet vaak problemen. Maar als je honderden van dergelijke processen hebt draaien gaat er wel dagelijks iets mis en loont het al snel de moeite. Dan zitten de developers op rotatiebasis gefrustreerd door logfiles te grasduinen, een inspanning die al snel het equivalent van een FTE kost. Overigens is het inzetten van zo’n robuuste oplossing best goed schaalbaar. Denk hierbij aan intelligente wrappers om je netwerk-, ETL en database code die bruikbare meldingen kunnen mailen naar de verantwoordelijke afdeling. Een bijkomend gunstig effect is ook dat fouten snel gesignaleerd en opgelost kunnen worden en niet als generiek incident van onbekende oorzaak gevlagd hoeven te worden. Beter voor ieders gemoedsrust.