HTTP Handlers Herzien
Je vindt alle code voor dit hoofdstuk hier
Dit boek bevat al een hoofdstuk over het testen van een HTTP-handler, maar dit zal een bredere bespreking van het ontwerp ervan bevatten, zodat ze eenvoudig te testen zijn.
We bekijken een praktijkvoorbeeld en hoe we het ontwerp ervan kunnen verbeteren door principes toe te passen zoals het principe van één verantwoordelijkheid en scheiding van belangen. Deze principes kunnen worden gerealiseerd met behulp van interfaces en dependency injection. Hiermee laten we zien dat het testen van handlers eigenlijk heel triviaal is.

Het testen van HTTP-handlers lijkt een terugkerende vraag te zijn in de Go-community, en ik denk dat het wijst op een breder probleem: mensen begrijpen niet hoe ze deze moeten ontwerpen.
De problemen die mensen met testen ondervinden, komen vaak voort uit het ontwerp van hun code in plaats van het daadwerkelijk schrijven van de tests. Zoals ik zo vaak in dit boek benadruk:
Als je tests je problemen bezorgen, luister dan naar dat signaal en denk na over het ontwerp van je code.
Een voorbeeld
Hoe test ik een http-handler die afhankelijk is van Mongodb?
Hier is de code
Laten we eens kijken wat deze ene functie allemaal moet doen:
HTTP-reacties schrijven, headers, statuscodes, enz. versturen.
De body van het verzoek decoderen naar een 'Gebruiker'.
Verbinding maken met een database (en alle details daaromheen).
De database bevragen en afhankelijk van het resultaat business logic toepassen.
Een wachtwoord genereren.
Een record invoegen.
Dit is te veel.
Wat is een HTTP-handler en wat moet hij doen?
Ik vergeet even de specifieke details van Go, maar ongeacht de taal waarin ik heb gewerkt, heeft het me altijd goed van pas gekomen om na te denken over de scheiding van belangen en het principe van individuele verantwoordelijkheid.
Dit kan best lastig zijn om toe te passen, afhankelijk van het probleem dat je oplost. Wat is precies een verantwoordelijkheid?
De grenzen kunnen vervagen, afhankelijk van hoe abstract je denkt en soms klopt je eerste gok niet.
Gelukkig heb ik met HTTP-handlers een redelijk goed idee wat ze moeten doen, ongeacht aan welk project ik heb gewerkt:
Een HTTP-verzoek accepteren, parsen en valideren.
Roep
ServiceThingaan omImportantBusinessLogicuit te voeren met de gegevens die ik uit stap 1 heb gehaald.Stuur een passend
HTTP-antwoord, afhankelijk van watServiceThingretourneert.
Ik zeg niet dat elke HTTP-handler ooit ongeveer deze vorm zou moeten hebben, maar 99 van de 100 keer lijkt dat voor mij het geval te zijn.
Wanneer je deze aandachtspunten scheidt:
Het testen van handlers wordt een fluitje van een cent en richt zich op een klein aantal aandachtspunten.
Belangrijk: het testen van
ImportantBusinessLogichoeft zich niet langer bezig te houden metHTTP; je kunt de bedrijfslogica schoon testen.Je kunt
ImportantBusinessLogicin andere contexten gebruiken zonder het te hoeven aanpassen.Als
ImportantBusinessLogicverandert wat het doet, hoef je je handlers niet te wijzigen, zolang de interface hetzelfde blijft.
Go's Handlers
Het type HandlerFunc is een adapter die het gebruik van gewone functies als HTTP-handlers mogelijk maakt.
type HandlerFunc func(ResponseWriter, *Request)
Lezer, haal even adem en bekijk de bovenstaande code. Wat valt je op?
Het is een functie die argumenten accepteert
Er zit geen frameworkmagie in, geen annotaties, geen magic beans, niets.
Het is gewoon een functie, en we weten hoe we functies moeten testen.
Het sluit mooi aan bij het bovenstaande commentaar:
Het accepteert een
http.Request, wat gewoon een bundel data is die we kunnen inspecteren, parsen en valideren. * > Eenhttp.ResponseWriter-interface wordt door een HTTP-handler gebruikt om een HTTP-respons samen te stellen.
Supereenvoudige voorbeeldtest
Om onze functie te testen, roepen we deze aan.
Voor onze test geven we een httptest.ResponseRecorder mee als ons http.ResponseWriter-argument, en onze functie gebruikt deze om het HTTP-antwoord te schrijven. De recorder neemt op (of bespioneert) wat er is verzonden, waarna we onze beweringen kunnen doen.
Een ServiceThing aanroepen in onze handler
ServiceThing aanroepen in onze handlerEen veelgehoorde klacht over TDD-tutorials is dat ze altijd "te simpel" en niet "realistisch genoeg" zijn. Mijn antwoord daarop is:
Zou het niet fijn zijn als al je code eenvoudig te lezen en te testen was, zoals de voorbeelden die je noemt?
Dit is een van de grootste uitdagingen waar we voor staan, maar waar we naar moeten blijven streven. Het is mogelijk (hoewel niet per se gemakkelijk) om code te ontwerpen, dus het kan eenvoudig te lezen en te testen zijn als we goede software engineering-principes oefenen en toepassen.
Samenvattend wat de handler van eerder doet:
HTTP-reacties schrijven, headers, statuscodes, enz. versturen.
De body van het verzoek decoderen naar een 'User'.
Verbinding maken met een database (en alle details daaromheen).
De database bevragen en afhankelijk van het resultaat bedrijfslogica toepassen.
Een wachtwoord genereren.
Een record invoegen.
Uitgaande van een meer ideale scheiding van belangen, zou ik het meer als volgt willen:
De body van het verzoek decoderen naar een 'User'.
Een 'UserService.Register(user)' aanroepen (dit is onze 'ServiceThing').
Als er een fout optreedt (het voorbeeld stuurt altijd een '400 BadRequest', wat ik niet juist vind), gebruik ik voorlopig gewoon een '500 Internal Server Error'-handler. Ik moet benadrukken dat het retourneren van '500' voor alle fouten een vreselijke API oplevert! Later kunnen we de foutafhandeling geavanceerder maken, bijvoorbeeld met error types.
Als er geen fout is,
201 Createdmet de ID als antwoordbody (wederom vanwege de beknoptheid/luiheid).
Om het kort te houden, zal ik het gebruikelijke TDD-proces niet bespreken; raadpleeg de andere hoofdstukken voor voorbeelden.
Nieuw ontwerp
Onze RegisterUser-methode komt overeen met de vorm van http.HandlerFunc, dus we kunnen aan de slag. We hebben het als methode gekoppeld aan een nieuw type UserServer, dat een afhankelijkheid bevat van een UserService, die wordt vastgelegd als een interface.
Interfaces zijn een fantastische manier om ervoor te zorgen dat onze HTTP-zorgen losgekoppeld zijn van een specifieke implementatie; we kunnen de methode gewoon aanroepen op de afhankelijkheid en hoeven ons niet druk te maken over hoe een gebruiker wordt geregistreerd.
Als je deze aanpak na TDD verder wilt verkennen, lees dan het hoofdstuk Dependency Injection en het hoofdstuk HTTP Server in de sectie "Een applicatie bouwen".
Nu we ons hebben losgekoppeld van specifieke implementatiedetails rondom registratie, is het schrijven van de code voor onze handler eenvoudig en volgt het de eerder beschreven verantwoordelijkheden.
De tests!
Deze eenvoud zie je terug in onze tests.
Nu onze handler niet meer gekoppeld is aan een specifieke implementatie van storage, is het voor ons triviaal om een MockUserService te schrijven waarmee we eenvoudige, snelle unittests kunnen schrijven om de specifieke verantwoordelijkheden uit te voeren.
Hoe zit het met de databasecode? Je speelt vals!
Dit is allemaal heel bewust gedaan. We willen niet dat HTTP-handlers zich bezighouden met onze bedrijfslogica, databases, verbindingen, enz.
Door dit te doen, hebben we de handler bevrijd van rommelige details en hebben we het ook gemakkelijker gemaakt om onze persistentielaag en bedrijfslogica te testen, omdat deze ook niet langer gekoppeld is aan irrelevante HTTP-details.
Het enige wat we nu nog hoeven te doen, is onze UserService implementeren met behulp van de database die we willen gebruiken.
We kunnen dit apart testen en zodra we tevreden zijn in main kunnen we deze twee units aan elkaar koppelen voor onze werkende applicatie.
Een robuuster en uitbreidbaarder ontwerp met weinig moeite
Deze principes maken ons leven niet alleen gemakkelijker op de korte termijn, ze maken het systeem ook gemakkelijker uit te breiden in de toekomst.
Het zou niet verwonderlijk zijn als we bij verdere iteraties van dit systeem de gebruiker een registratiebevestiging per e-mail zouden willen sturen.
Met het oude ontwerp zouden we de handler en de omliggende tests moeten aanpassen. Dit is vaak de reden waarom delen van de code niet meer te onderhouden zijn, er sluipt steeds meer functionaliteit in omdat het al zo ontworpen is; zodat de "HTTP-handler"... alles kan afhandelen!
Door aandachtspunten te scheiden met behulp van een interface hoeven we de handler helemaal niet te bewerken, omdat deze zich niet bezighoudt met de bedrijfslogica rondom registratie.
Samenvattend
Het testen van Go's HTTP-handlers is niet uitdagend, maar het ontwerpen van goede software kan dat wel zijn!
Mensen maken de fout te denken dat HTTP-handlers speciaal zijn en gooien goede software engineering practices overboord bij het schrijven ervan, wat het testen ervan vervolgens lastig maakt.
Nogmaals: Go's HTTP-handlers zijn gewoon functies. Als je ze schrijft zoals je andere functies zou schrijven, met duidelijke verantwoordelijkheden en een goede scheiding van taken, zul je geen moeite hebben met het testen ervan en zal je codebase er gezonder door worden.
Laatst bijgewerkt