Bij het realiseren van een microservices architectuur is één van de uitdagingen het doortesten van één of meerdere services. Zoals menig developer weleens ervaren heeft, is het opzetten van integratietesten niet triviaal. Dergelijke testen vergen vaak complexe mocking, veel setup en boilerplate code en dit gaat ten koste van executiesnelheid en gemak. Veel frameworks hebben in het verleden vereenvoudigingen geboden, maar nu is er REST-assured! In combinatie met WireMock wordt in dit artikel getoond dat het schrijven van integratietesten weer fun kan zijn!
Situatie
Bij het project Hypotheekdossier van de Rabobank realiseren we een aantal services, die gezamenlijk functionaliteit aanbieden in een mobiele app. De app zelf wordt gerealiseerd met het React/Redux framework en voor de backend hanteren we primair Spring Boot services, die gedeployed worden op Mesos/Marathon. Indien services een database vereisen, integreren ze met een ElasticSearch database. Ook vereisen een aantal services connecties met services, die buiten de Rabobank worden aangeboden. Zie figuur 1 voor deze architectuur.
Figuur 1: Architectuur plaat Services
Binnen deze architectuur gebruiken we veel services, waardoor het belang van het testen van deze services onderling nog belangrijker is. Om toch de kwaliteit van elke service te garanderen, hebben we ervoor gekozen om veel integratietesten te maken. Aangezien elk endpoint van een service een duidelijk gekaderd functioneel doel dient, helpt dit bij het bedenken van correcte test-cases.
Initieel was gestart met Cucumber, die de testcase beschreef. Middels zogenaamde glue code wordt vervolgens een REST service aangeroepen met de juiste parameters. Zie Figuur 2 voor een overzicht met mock code.
Figuur 2: Cucumber test voor een REST service
Dit werd snel verbose en lastig te onderhouden over alle services heen. Dit komt mede door de driestap sprong: feature file, glue code en generic HTTP class. Om toch grip te houden op de integratietesten is er gekozen voor REST-assured. REST-assured is niet meer dan een HTTP client, dat aan voelt als een DSL (Domein Specific Language). Middels deze aanpak was het mogelijk om dezelfde integratietest vele malen eenvoudiger op te zetten.
Een opzet met REST-assured lijkt op Unit-Test met 5 regels code. Zie Figuur 3 als voorbeeld. Wat opvalt is dat er weinig van de expressiviteit van de feature verloren is gegaan met minder code, terwijl de test hetzelfde uitvoert als de Cucumber test.
Figuur 3: REST-assured test voor een REST service
Dit artikel zal aan de hand van een aantal voorbeelden de kracht van de REST-assured DSL laten zien.
Requests opstellen
Om op te starten met REST-assured heb je de volgende maven dependency nodig: io.rest-assured:rest-assured:3.0.1. Ook kun je eventueel een aantal star-imports toevoegen, zoals beschreven in Listing 1.
Listing 1: opzet voor REST-assured
Vervolgens is het mogelijk om in je testfile REST-assured requests te maken. Hierin hanteert REST-assured twee smaken: een rechtdoorzee one-liner en een vorm van Behaviour Driven Development. Allebei bieden ze dezelfde mogelijkheden, zoals beschreven in Listing 2.
Listing 2: one-liner en BDD varianten van requests
De eerste smaak is fijn om te hanteren bij het opzetten van je test, zoals bijvoorbeeld een standaard request sturen om een account aan te maken. Deze roep je dan in een `@Before` aan. Vervolgens zou je in de daadwerkelijke test middels de BDD-smaak de daadwerkelijke test uitvoeren.
Dit helpt om op een gestructureerde wijze het request op te vragen. Koppel dit met een aantal keer gebruik te maken van de IDE auto completion en de ervaring is dat je hiermee voldoende op weg bent in het opstellen van een request.
Een uitgebreider voorbeeld staat beschreven in Listing 3. In dit voorbeeld wordt gevraagd aan het spel League of Legends om de data omtrent de champion “Aurelion Sol: The Star Forger”.
Listing 3: Aanroep van League of Legends API
Wat opvalt is dat je erg vrij wordt gelaten in de stijl, die je wilt hanteren. De `.and()` is niets meer dan syntactic sugar en voegt enkel leesbaarheid toe. De `.param("", "")` en `.queryParam("", "")` zijn hetzelfde. Ze voegen allebei een query parameter toe aan het request. Dit biedt als extra voordeel dat het niet alleen voor een developer leesbaar is, maar ook voor een tester, die bekend is met HTTP. Naast de `GET` ondersteunt REST-assured ook alle andere HTTP methodes. Je dient dan simpelweg de `.get(url)` te vervangen voor de gewenste methode. Bij Listing 4 zie je hiervan een aantal voorbeelden.
Listing 4: Andere request methoden
Nu begint het al vervelend te worden, dat in iedere test de hele URL dient te worden opgeven.
Hiervoor biedt REST-assured een aantal static properties om altijd de juiste waardes te pakken.
Deze zijn te vinden op het RestAssured object. Logischerwijze is het handig om deze in te stellen in de `@BeforeClass` methode, zoals is aangeduid in Listing 5.
Listing 5: Basisinstellingen van REST-assured aanpassen
In het opbouwen van het request kunnen we nu REST-assured aanvullen met configuratie, meerdere smaken gebruiken, parameters toevoegen en deze loggen met `.then().log().all()`.
Nu blijft er echter nog een heikel punt over. Hoe kunnen we een object als text e.d. sturen in een post als JSON, XML en FormData? REST-assured biedt hiervoor de `.body()` en de `.formParam()` methodes. Om het object te serialiseren, hanteert REST-assured standaard Jackson. Mocht deze niet op het classpath aanwezig zijn, dan valt REST-assured terug op Gson en als laatste optie kiest REST-assured JAXB. Op basis van het gegeven content-type zal het object worden geserialiseerd naar JSON, dan wel XML. Voorbeelden hiervan zijn te zien in Listing 6.
Listing 6: Toevoegen van objecten aan een POST request
Dezelfde toegankelijkheid biedt REST-assured ook voor het opgeven van cookies en het toevoegen van autorisatie. Namelijk door het toevoegen van `.cookie("yummie", "cookies")` of de `.auth().basic("username", "password")` na het declareren van de given stap. Een voorbeeld hiervan is te zien in Listing 7.
Listing 7: Request met basic authentication en cookie
Validatie
We hebben nu mooie requests die het flitsend goed doen, maar we hebben eigenlijk nog niets gevalideerd. Ook doen we nog niets met het terugkomende resultaat van het verstuurde request. Om het resultaat terug te krijgen van het uitgevoerde request, kan je de `.extract()` methode aanvullen. Het is dan mogelijk om te kiezen om meerdere delen van het request lost te peuteren, zoals de body van het HTTP response (zie Listing 8).
Listing 8: Response body extraheren
Bij deserialisatie wordt dezelfde werkwijze gehanteerd als bij de serialisatie van een object in een POST bericht. Veelal is het niet gewenst om het antwoord op deze manier te valideren. Beter is het om de standaard ingebouwde GsonPath te gebruiken in combinatie met Hamcrest. Dit is een soort van CSS selector/XPath, die het mogelijk maakt om specifieke waardes uit een bericht te halen en deze te controleren. Dit zorgt voor korte code, waarbij je beter specifieke cases kunt testen. Om hier een beter beeld van te schetsen, is er een voorbeeld gegeven in Listing 9. Hierin pakken we het antwoord van League of Legends en controleren we een aantal waardes van onze champion.
Listing 9: Uitgebreidere validatie op een request middels GsonPath
Met andere woorden: doodeenvoudig! Je vertelt in welke JSON key je interesse hebt en welke validatie hierop moet worden aangeroepen. Voor de validatiemethodes kan je gebruiken maken van de Hamcrest matchers. Deze syntax voorkomt een hoop boiler-plate, zoals het deserialiseren van het response om deze vervolgens te valideren.
Dit kan je op precies dezelfde manier toepassen als het antwoord een XML-document was geweest. Mocht je toch dringende behoefte hebben om op het XML-bericht een XPath los te laten, dan kan je dat doen middels `.body(hasXPath(""))`.
Naast het valideren van specifieke waardes in een bericht kan je er ook voor opteren om te valideren middels een schema. Voor JSON wordt de JSON Schema vocabular gebruikt. Hierbij hoort de dependency `io.rest-assured:json-schema-validator`. Hierna kan je ergens op het classpath een JSON-bestand toevoegen, die voldoet aan JSON Schema. Het is dan mogelijk om het request te valideren middels REST-assured door de volgende regel: `.body(matchesJsonSchemaInClasspath(“lol-champion-schema.json")`.
A note on mocking: WireMock
We kunnen nu eenvoudige HTTP requests opstellen en het antwoord hiervan valideren. Hierbij wordt aangenomen dat de service, die wordt getest, al staat te pruttelen en goed benaderbaar is. Veelal is dit niet zo eenvoudig. De applicatie moet ergens werken en als de applicatie gebruik maakt van koppelingen, zal daar ook een oplossing voor moeten zijn. Om dit makkelijker te maken, kan je gebruik maken van de tool WireMock. WireMock is namelijk een mock HTTP-server.
Met WireMock is het mogelijk om een request te definiëren met een bijbehorend response. Vervolgens start WireMock een Netty instantie. Als de applicatie nu een vraag stelt aan WireMock zal hij het correcte response ontvangen. Om WireMock te gebruiken, dien je de dependency `com.github.tomakehurst: wiremock` op te nemen. Nu is het mogelijk om WireMock te gebruiken in een test. Zie Listing 10 voor een voorbeeld.
Listing 10: Opzet van een WireMock test
Aan dit voorbeeld vallen een aantal dingen op: Start WireMock, mock request met WireMock, voer test uit, stop WireMock. Om dit wat te vereenvoudigen kan je gebruik maken van de `WireMockRule` in combinatie met de `@Rule` annotatie van JUnit. Zie Listing 11.
Listing 11: WireMock als JUnit rule
Nu zal er aan het begin van alle testen WireMock worden gestart, na alle testen wordt WireMock gestopt en per test worden alle gemockte testen op WireMock gereset.
Dit zorgt ervoor dat je service testen geïsoleerd zijn en niet toevallig de vorige WireMock configuratie kan raken.
Om een gemockt request op te bouwen, dien je eerst aan te geven op welke path WireMock iets dient terug te geven en vervolgens definieer je wat voor response WireMock geeft. Zie Listing 12 voor een simpel voorbeeld.
Listing 12: Een gemockt request met WireMock
Conclusie
Het schrijven van integratietesten is veelal niet makkelijk. Het vergt vaak complexere mocking, veel setup en glue code om de daadwerkelijke test uit te kunnen voeren. In dit artikel heb ik laten zien dat REST-assured veel glue code overbodig maakt. Dat een developer beschikt over een krachtige, natuurlijke DSL waarmee snel functionele testen geautomatiseerd kunnen worden. Dit in combinatie met WireMock zorgt voor een krachtige combinatie voor het goed doortesten van een REST service.