Als er meer dan één consumer is, schrijven ze elk hun eigen contract. Dit maakt de aanpak perfect voor fijn afgestemde interfaces met een of twee consumers.
Het contract
Een consumer-based contract bevat request-response paren voor de interface, in de vorm van ‘If I request A, the interface must respond with B’. Hierdoor kan het contract bepalen hoe de interface eruit moet zien en hoe deze zich moet gedragen. Alleen de consumer kan het contract aanpassen.
Request-response paren zullen bekend voor komen voor iedereen die wel eens stubs heeft geschreven. Dit is namelijk hoe de meeste stub-frameworks werken. De consumer kan hiermee zijn contract gebruiken als bron van stubs. Tegelijkertijd zal de provider het contract gebruiken als bron van tests.
Consumer-based contracten zijn op voorbeelden gebaseerd. Op zichzelf staand zijn ze daarom geen geweldige documentatie voor mensen. Maar niet getreurd: gooi er wat tooling overheen en je krijgt geweldige documentatie, vol met voorbeelden.
Een kwalitatief consumer-based contract …
- Bevat alle eisen die de consument aan de interface stelt.
- Bevat zo min mogelijk. Neem alleen de vereiste onderdelen van de interface op. Laat alles weg wat de applicatie niet nodig heeft. Hierdoor kan de provider op data gebaseerde beslissingen nemen over hun interface.
- Houdt zich aan alle standaarden en richtlijnen. Denk aan de contractstandaard, bedrijfsstandaarden en algemene best practices.
Waar te beginnen?
Door contract-first te werken, kunnen we het contract gebruiken als de specificaties van de interface. De consumer begint met het definiëren van alle vereisten in het contract. Daarna begint de provider met het implementeren van de interface. Dit levert voor beide partijen een enorm procesvoordeel op. Het elimineert veel onzekerheden, aannames en miscommunicaties tijdens het ontwikkelingsproces.
Als je implementation-first werkt, is de consumer-driven benadering niet de juiste keuze. Het biedt geen grote voordelen ten opzichte van de provider-driven benadering, terwijl het wel aanzienlijk meer inspanning kost aan de provider-kant. Maar, het is nooit zo simpel. Er zijn enkele situaties waarin implementation-first werken wél kan werken. Proceed with caution.
Verantwoordelijkheden van de consument
De consumer heeft in elke fase een aanzienlijke macht over de interface. Ze kunnen zowel de interface als de workflow van de provider beïnvloeden. Maar, macht komt met verantwoordelijkheden. De verantwoordelijkheden van de consument omvatten:
- Schrijven van het contract
Onderdeel hiervan is ervoor te zorgen dat het contract van hoge kwaliteit is. Een kwalitatief contract leidt direct tot hoogwaardige contracttesten en een gestroomlijnde interface. Bij het opstellen van het contract is het belangrijk om je bewust te zijn van de impact die dit kan hebben op de provider. Een kleine verandering van een consumer kan grote gevolgen hebben voor de provider. - Verbinding maken met de interface
Logisch: dat is de definitie van wat een consumer is. - Testen van de consumer applicatie met het contract
De consumer zorgt ervoor dat zijn applicatie alles aankan wat in het contract is gedefinieerd. Hierdoor kan de provider er veilig van uitgaan dat alles wat de consumer nodig heeft voor de interface, correct is vastgelegd in het contract. Als je deze stap overslaat, kan dit ertoe leiden dat de provider het verkeerde implementeert.
Deze stap maakt het contract ook een integraal onderdeel van de consuming applicaties tests. Deze koppeling zorgt ervoor dat de consumer zijn contract bijwerkt wanneer hun eisen veranderen. - Publiceren van het contract
De laatste verantwoordelijkheid van de consument is om het geteste, kwalitatieve contract in de centrale contract repository te publiceren.
Verantwoordelijkheden van de provider
Bij de consumer-driven benadering begint de aanbieder met een aanname: het contract bevat alles wat de consumer nodig heeft van de interface. Niets meer en niets minder. De provider bouwt een interface die werkt met alle contracten. Hun verantwoordelijkheden omvatten:
- Downloaden van alle laatste contracten
Bij elke test run downloadt de provider het laatste contract van elke consument van de centrale contract repository. Dit zorgt ervoor dat de provider nooit test met een ontbrekend of verouderd contract. - Implementeren op basis van de contracten
De provider bouwt zijn interface zodat hij alles kan doen wat in de contracten staat beschreven. De provider schreeuwt “YAGNI! KISS!” bij het toevoegen van dingen die niet in een contract staan beschreven. Dit houdt de interface schoon. - Test de implementatie met de contracten
De provider gebruikt de request-response paren in de contracten voor black-box testen. Dit zorgt ervoor dat de interface-implementatie voldoet aan de eisen van de consumer. Als een test faalt, moet de provider ervan uitgaan dat dit een productieprobleem veroorzaakt voor de consumer. Een falende test moet daarom altijd een release stoppen. - Beheer
De provider heeft de verantwoordelijkheid om alle tests te laten slagen. Dit omvat het onderhouden van testafhankelijkheden zoals data, bestanden, stubs, andere contracten, enzovoorts. Hierdoor vergroot elk contract de testbelasting op de provider. Dit maakt het moeilijk om het aantal consumers op te schalen.
Een interessant effect van deze aanpak is dat de provider veel feedback krijgt van de consumer via hun contracten. Wanneer de provider een deel van de interface verwijdert, zal de contracttest precies laten zien wie er gebruik van maakt. De provider kan bijvoorbeeld achterhalen wie dat ene deprecated deel van de interface nog gebruikt. Na al die mailtjes! De contracten bevatten waardevolle gebruiksgegevens. De provider kan deze gebruiken om op data gebaseerde beslissingen te nemen over de interface.
Een ander opmerkelijk effect is dat de provider bij deze aanpak minder tests hoeft te schrijven. Technische en enkele functionele tests worden door consumers geschreven. Dit betekent echter niet dat de provider helemaal niet hoeft te testen: ze moeten de black-box-tests van het contract namelijk aanvullen met eigen tests.
Voordelen
De consumer-based benadering is een goede benadering om aan beide kanten zowel de technische als enige functionele correctheid te waarborgen. Het aantal functionele dingen dat met deze aanpak getest kan worden, is afhankelijk van de applicatie.
Met consumer-based tests wordt alles lokaal uitgevoerd. Dit zorgt voor een snelle testuitvoering aan beide kanten. Het maken van testen is snel aan de consumer-kant omdat ze volledige controle hebben over alle delen van de tests. Aan de provider-kat is het iets langzamer, omdat de consumers de tests maken. De provider heeft controle over de tests (hier later meer over).
De consumer-driven benadering is op procesniveau geweldig. Consumers hebben veel controle over de interface. Het stelt hen in staat om heel duidelijk en heel precies te definiëren wat de interface moet doen. Ze definiëren dit in de gemeenschappelijke taal; de contractstandaard. Voor iedereen die de gemeenschappelijke taal spreekt, is het duidelijk waartoe de interface in staat moet zijn.
Vanuit het oogpunt van de provider definiëren de consumers de interface met tests. Dit geeft de provider de procesvoordelen die komen met test-driven development (TDD), maar zonder het snel cyclische karakter. De contracttesten bevatten gebruiksgegevens voor elke consumer. Door deze data te gebruiken, kan de provider op data gebaseerde beslissingen nemen over de interface.
"Met consumer-based tests wordt alles lokaal uitgevoerd. Dit zorgt voor een snelle testuitvoering aan beide kanten."
Nadelen
De consumers hebben bij deze aanpak veel controle over de interface. Dat is over het algemeen een goede zaak voor consumers, maar het kan ook een slechte zaak zijn voor de provider. De provider heeft volledige controle over alle testafhankelijkheden, maar de consumers beheersen de tests zelf. Dit creëert een afhankelijkheid tussen de provider en al hun consumers. De meeste problemen die hierdoor ontstaan, kunnen alleen worden opgelost door met elkaar te communiceren. Het vergroten van de afhankelijkheid op consumers kan impact hebben op de autonomie van de provider.
Het grootste voorbeeld hiervan is wanneer een consumer een slecht contract publiceert. Een simpele typefout van een consument kan een falende test aan de provider-kant veroorzaken. Hierdoor is de provider niet meer in staat naar productie te releasen. Ook niet bij beveiligingspatches en bugfixes. De provider moet wachten tot de consumer zijn contract heeft gemaakt voordat ze verder kunnen gaan met hun release.
Bij de consumer-driven benadering kan het moeilijk zijn om het aantal consumers op te schalen. De provider moet ervoor zorgen dat alle contracttesten slagen. Hierdoor neemt de testbelasting op de provider toe bij iedere consumer. De testbelasting voor een enkele consumer is zeer beheersbaar, maar testafhankelijkheden voor 10 consumeners is een heel ander verhaal. Bovendien kan het hebben van meerdere consumers die onafhankelijk tests schrijven, resulteren in …
- Overbodige tests
Sommige dingen worden meerdere keren getest. In de praktijk zou het effect hiervan echter minimaal moeten zijn vanwege een snelle testuitvoering. - Tegenstrijdige tests
Meerdere tests met dezelfde input maar een andere output kan het onmogelijk maken dat alle tests slagen. Nogmaals, communicatie is de enige manier om dit op te lossen.
Conclusie
Bij de consumer-based benadering begint alles bij de consumer. Elke consumer is verantwoordelijk voor het opstellen van een kwalitatief contract. Het contract definieert de functionaliteit van de interface. De consumers delen het contract met de provider via de centrale contract repository. De provider downloadt iedere test run de laatste contracten. Ze gebruiken deze contracten om hun interface te implementeren en te testen.
Deze aanpak zorgt voor technische én enige functionele correctheid. Op deze manier werken is daarmee perfect voor het bouwen van nauwkeurig afgestelde interfaces zonder ongebruikte functionaliteit. Het is niet goed in het opschalen van het aantal consumenten. Bovendien kunnen sommige problemen in deze benadering alleen worden opgelost door de provider met zijn consumers te laten communiceren. Dit maakt de aanpak niet geschikt voor op het internet beschikbare interfaces.
De consumer-driven benadering is een krachtige manier van werken om een interface te bouwen met een paar consumenten. Wanneer je het gebruikt, moet je je echter wel bewust zijn van de tekortkomingen en inherente nadelen.
Nu we zowel de provider-based als de consumer-based benadering hebben bekeken, kunnen we ze met elkaar gaan vergelijken. In het volgende deel vergelijk ik de provider-driven met de consumer-driven benadering.
Met dank aan Vilas Pultoo voor de illustraties