Wat is Contract Testing? Wellicht heb je de term al eens gehoord en misschien ben je er ook al een keer mee in aanraking geweest. Voordat ik hier verder op in ga, wil ik eerst nog wat context geven [Roger Stinissen]
Veel organisaties hebben hun way of work al aangepast van waterval naar Agile DevOps. Echter, er wordt nog wel gebruikgemaakt van een DTAP straat (development, test acceptatie, productie straat) waar vaak minder beheer op plaatsvindt dan tijdens waterval. Tegenwoordig doen teams veelvuldig aanpassingen en deployments van hun software en gaan ze in een hogere frequentie door de DTAP straat. Daar waar je nog autonomie en snelheid hebt in een eigen DEV of local omgeving, loop je nu vaak stuk in de (keten) testomgeving. Zoals downtime vanwege de veelvuldige deployments of doordat interfaces afwijken van de services waar je op aan wilt sluiten. Kortom: ellende!
Waar je tijdens het coderen volledig autonoom bent, wil je dit eigenlijk ook bereiken met het testen. Afgezien van de unit testen, vinden de meeste testen pas plaats in een (keten) testomgeving. Terwijl de meeste testen veel eerder uitgevoerd kunnen worden. Met name op het gebied van integratie komen er vaak issues naar voren in een testomgeving. Deze issues zou je graag in een eerder stadium willen tackelen, zonder dat hele ketens eruit liggen en achterhaald moet worden waar het probleem zit en wie dit heeft veroorzaakt. Er is een manier om dit te voorkomen, namelijk contract testing!
Wat is Contract testing?
Met contract testing controleer je of services met elkaar kunnen communiceren op basis van afspraken die vastgelegd zijn in een contract. Vaak zijn dit request en response berichten die via HTTP communiceren, maar dit kunnen ook berichten op message queues zijn. Afspraken over de interface die je normaal gesproken vastlegt in een tool of wellicht in de mail, leg je nu vast in een contract. Op basis van het contract worden testen opgesteld die het contract verifiëren. Je kunt je voorstellen dat een functional OK een andere response oplevert qua interface dan een functional NOK. Daarnaast kun je ook nog technische of authenticatie errors verifiëren.
Bij Contract testing is er altijd sprake van één of meerdere consumers en een provider. In een keten kan de provider ook weer consumer zijn. Vaak is er ook nog een broker bij betrokken waarop de contracten gepubliceerd worden en waarop de resultaten getoond worden van de uitgevoerde testen.
Figuur 1. Schematische weergave Consumer Driven Contract test.
Consumer Driven Contracts
In geval van Consumer Driven Contract testing stelt de consumer de testen op en genereert zijn eigen contract, die hij middels een broker deelt met de provider zoals weergegeven in figuur 1. Vaak zijn er meerdere consumers van een service. Het voordeel is dat de consumer alleen datgene test dat daadwerkelijk wordt afgenomen van de provider. Zo test elke consumer zijn eigen deel van de service die van een provider wordt afgenomen. Het voordeel voor de provider is dat hij inzichtelijk krijgt wie wat afneemt van zijn service. Hierdoor kan de provider gemakkelijk aanpassingen doen in onderdelen die niet worden afgenomen van zijn service. Mocht hij toch een aanpassing willen doen in onderdelen die wél worden afgenomen, dan heeft hij meteen inzichtelijk voor wie dit impact heeft en kan hij daar op acteren.
Hoe ziet een contract eruit?
Hierbij een voorbeeld van een contract dat aan de hand van een test wordt gegenereerd. In dit geval is het PACT framework gebruikt. Zoals je ziet, staat er een request en een response in die beide gevuld zijn met data. Bij Contract Testing ligt de nadruk op de interface, maar om een valide test te kunnen doen, moeten zowel de request als de response gevuld worden met valide data. Deze data moet ook aanwezig zijn in de gehele keten aangezien de provider de testen valideert tegen zijn eigen code. Als de gehele keten is uitgevoerd in een contract test, moet ook de data consistent doorgevoerd worden in deze keten. Uiteraard is het ook mogelijk om onderliggende services te mocken, maar dan wel met consistente data.
In figuur 2 staat een interactie met een functioneel OK situatie beschreven. Er zijn uiteraard ook interacties waarbij een request tot een functioneel NOK kan resulteren en dat leidt tot een andere interface. Deze situaties moeten ook in het contract opgenomen worden.
In het contract staan drie belangrijke zaken, namelijk de provider en de consumer met bijbehorende naam en een interactie. De interactie heeft een beschrijving en bestaat uit een request en response. In het request vul je naast de method ook het endpoint, de headers en eventueel nog een body in. De response bevat een HTTP status, headers en een body. Aangezien het contract vanuit consumer oogpunt is geschreven, neem je alleen die onderdelen van de body op die relevant zijn. In figuur 1 zijn alleen country en language relevant voor deze consumer.
Figuur 2. Voorbeeld contract.
Broker
Je kunt op je eigen machine zowel de consumer en de provider draaien of jouw contracten naar de provider te sturen. Uiteindelijk wil je alles in de pipeline opnemen en de testen onderdeel van de pipeline laten zijn. In bijna alle gevallen zal een broker gebruikt worden. De broker geeft je inzicht welke contracten er zijn met welke versies. Daarnaast kun je precies zien wanneer welke contracten zijn geverifieerd. Figuur 3 is een voorbeeld van een PACT broker waarbij zowel een succesvolle als een niet-succesvolle validatie heeft plaatsgevonden. Uiteraard kun je op de broker ook zien waarom een test heeft gefaald. Daarnaast geeft de broker ook inzicht in de relaties van een consumer of een provider. Als je een contract test voor een volledige keten hebt, dan toont de broker de volledige grafische weergave van deze keten. In figuur 4 staat een simpele grafische weergave. Maar hoe meer relaties er zijn, hoe groter de grafische weergave wordt.
Figuur 3. Run overzicht Broker.
Zodra de consumer de contracten gereed of aangepast heeft, publiceert hij deze op een broker. Vervolgens haalt de provider de contracten op van de broker en valideert deze. De resultaten van de validatie worden ook op de broker gepubliceerd zoals op figuur 3 te zien is. Via webhooks kunnen zowel consumer als provider getriggerd worden aan de hand van events, zoals gewijzigde contracten of resultaten van de validaties van de provider. Aan de hand van deze events zou het kunnen betekenen dat er een bug zit in de code en dat deze gefixed moet worden.
Figuur 4. Grafische weergave relatie provider – consumer.
Voordelen
Naast het grote voordeel dat je lokaal kunt controleren of je tegen een breaking change aanloopt naar aanleiding van code aanpassingen, kent contract testing nog een aantal voordelen:
- Root cause is makkelijker te constateren;
- Snellere feedback dan integratie testen op een omgeving;
- Geen omgevings issues die je wel met integratie testen hebt;
- Test cases die normaal gesproken in een End-2-End of integratie test plaatsvinden, zijn overbodig.
Het belangrijkste is misschien wel dat je onafhankelijk van jouw omgeving kunt ontwikkelen en testen zonder dat je hoeft te wachten op andere engineers of teams. Waar je voorheen altijd tegen een werkende service aan moest testen of een zelfgefabriceerde stub, test je nu jouw code tegen een interface die gevalideerd is. Dit voorkomt heel veel communicatie en andere tijdrovende, handmatige acties die je wel in een integratie- of ketentest hebt. Het contract is namelijk de waarheid en niet een notitie in een mail van een aantal maanden geleden.
Niet aan beginnen
Zijn er ook situaties waarbij je geen tijd in contract testing moet investeren? Ja, als jij als consumer een contract wilt opstellen, maar er is geen provider te vinden die dit ook wil dan heeft het weinig zin om aan de slag te gaan met contract testing. Zowel consumer als provider moeten hier serieus mee aan de slag gaan, anders creëer je meteen een nieuwe legacy. Vanuit provider oogpunt wil je daarnaast dat alle consumers een contract test hebben. Dus als je eraan begint, zorg er dan voor dat je meteen alle afnemers meeneemt. In de meest optimale situatie zou je de gehele keten in een contract willen hebben. Contract testing moet het ontwikkelproces versnellen. Het is dus logisch dat als je contract testing implementeert, dat je dit ook in de CI pipeline opneemt.
Test Strategie
Tegenwoordig worden veelal kleine changes doorgevoerd, maar de frequentie van de changes is wel omhoog gegaan. Door het uitvoeren van een goed dekkende unit test en een contract test kun je de meeste ketentesten overbodig maken. Uiteraard is het afhankelijk van het risico dat het met zich mee brengt als software geïmplementeerd wordt in productie. Een organisatie die een groot risico loopt op negatieve publiciteit of grote financiële gevolgen ondervindt van onjuist werkende software, heeft een heel andere test aanpak nodig dan een organisatie waar dit niet het geval is. Het is dus aan te raden om goed te kijken naar de risico’s en aan de hand hiervan een teststrategie op te stellen. Hierin definieer je de verschillende testsoorten op basis van de risico’s die voorzien zijn. Met een strategie neem je een weloverwogen besluit om bepaalde testsoorten wel of niet te doen en creëer je voor iedereen duidelijkheid. Bovendien zul je zien dat de unit testen en contract testen standaard onderdeel worden bij een release en dat tijdrovende ketentesten veelal worden ingezet bij journeys met een hoog risicogehalte.
Wil je zelf aan de slag met contract testing? Neem dan eens een kijkje op de volgende websites:
https://spring.io/projects/spring-cloud-contract
Roger Stinissen is sinds 1998 actief in de IT. Sinds 2014 werkt hij bij ING in de functie van DEV engineer. Hij ondersteunt teams in het testen van hun applicaties met onder andere contract testing, maar ook door ze te trainen in testtechnieken.