Å "fake" avhengighetene til applikasjonen din er en investering som betaler seg raskt. Det sparer deg for tid under lokal utvikling, og gjør det betydelig enklere å skrive gode og raske tester.
Moderne tjenester lever sjelden i et vakuum. De fleste applikasjoner vi utvikler og vedlikeholder har avhengigheter – enten det er kall til andre APIer, lagring av data i en database, eller sending av meldinger til køsystemer som Kafka.
Denne artikkelen beskriver hvorfor vi bør lene oss tyngre på integrasjonstester for mikrotjenester. Dette er tester hvor vi kjører opp hele tjenesten for å verifisere at f.eks. et API fungerer som forventet med ulik input. Slike tester tilrettelegger for refaktorering uten at testene brekker konstant, og de verifiserer at applikasjonen faktisk fungerer i samspill med omverdenen. Men de gjør også at vi må håndtere de ulike avhengighetene som applikasjon er avhengig av når den kjører.
Dermed står vi overfor et valg: Hva gjør vi med avhengighetene? Skal vi koble oss til lokale versjoner av de andre tjenestene? Skal vi kjøre opp tjenestene med Docker eller bruke Wiremock? Skal vi gå mot et testmiljø? Eller skal vi "fake" dem ut?
Hva er egentlig fakes?
Det finnes mange definisjoner på fakes og andre typer "test doubles" som mocks og stubs, men kort fortalt er en fake et objekt med en fullt fungerende implementasjon. Den oppfører seg som den ekte implementasjonen, men er kraftig forenklet for bruk i tester og lokal kjøring.
Fakes brukes typisk til å simulere avhengigheter som eksterne APIer, databaser eller køer. I en integrasjonstest vil dermed applikasjonen starte opp som normalt, men være konfigurert til å bruke fakes i stedet for de ekte implementasjonene. Dette gir deg som utvikler full kontroll over hvordan omverdenen svarer – og hvordan applikasjonen din håndterer det.
Hvordan gjør man det?
For å lage en fake starter du typisk med å definere et interface for typen du ønsker å fake. Hvis du for eksempel har en HTTP-klient som henter data, lager du to implementasjoner av dette interfacet; en for den ekte kommunikasjonen, og en fake som f.eks. returnerer data fra et map eller en liste.
Nedenfor vises et enkelt eksempel hvor vi har en PersonClient som henter info om personer basert på en identifikator fra en annen tjeneste. Fake-implementasjonen har et enkelt map som inneholder noen personer og returnerer disse hvis man sender inn en id som finnes.
interface PersonClient {
fun hentPerson(id: String): Person?
}
// Faktisk implementasjon som gjør HTTP-kall
class DefaultPersonClient : PersonClient {
override fun hentPerson(id: String): Person? {
// Implementer http-kall til annen tjeneste
}
}
// Fake implementasjon som returnerer data fra et map
class FakePersonClient : PersonClient {
private val data = mapOf("1" to Person(...))
override fun hentPerson(id: String) = data[id]
}Dette prinsippet fungerer også utmerket for andre typer implementasjoner som f.eks. databaselaget. Selv om verktøy som Testcontainers lar deg kjøre ekte databaser under kjøring av tester, tar de gjerne noe tid å starte opp og gjør at testene kan ta en del tid å kjøre, spesielt etter hvert som antall tester begynner å øke. Ved å bruke fakes for store deler av integrasjonstestene kan du spare mye tid. Så kan du heller supplere med noen spesifikke databasetester som verifiserer at SQL-spørringene dine faktisk fungerer som forventet.
Simulering av feil
En fake kan gjøre mer enn bare å returnere data. Du kan utvide den for å enkelt simulere spesialtilfeller eller feilsituasjoner som er vanskelige å gjenskape mot et ekte system.
Eksempelet nedenfor viser en enkel måte å sette fake-implementasjonen i en tilstand som gjør at den vil returnere en feil når man prøver å hente data.
// Fake implementasjon med feil-simulering
class FakePersonClient : PersonClient {
var skalFeile: Boolean = false
override fun hentPerson(id: String): Person? {
if (skalFeile) throw RuntimeException("Feil fra baksystem")
return data[id]
}
}
fun test() {
// I testen kan man simulere at clienten skal feile
fakePersonClient.skalFeile = true
// Kjør test og verifiser at applikasjonen håndterer feilen korrekt
}Konfigurasjon
Når fake-implementasjonene er på plass, må du sørge for at testene dine faktisk bruker de. Avhengig av teknologistacken din, kan dette f.eks. løses ved hjelp av profiler i Spring, properties som settes i applikasjons-konfigurasjonen eller custom kode. Poenget er at man må sørge for at de ulike fake-objektene blir "injectet" i stedet for de ekte implementasjonene.
Hvorfor bruke fakes?
Hovedgevinsten henter du i integrasjonstestene. Fakes gir deg deterministiske tester som kjører raskt og stabilt.
Men det er også en stor fordel for lokal utvikling. Ved å bytte ut ekte integrasjoner med fakes, kan du kjøre opp hele applikasjonen lokalt uten å spinne opp mange andre mikrotjenester eller være avhengig av eksterne testmiljøer.
- Du kan kjøre applikasjonen din uten Internett eller VPN-tilkoblinger.
- Du kan enkelt manipulere tilstanden i fakene for å teste hvordan applikasjonen din oppfører seg i ulike scenarier rett fra din egen maskin.
- Du trenger ikke lagre credentials eller passord på egen maskin for å autentisere deg mot eksterne systemer.
Hva med ulemper?
Ingen løsning er uten kostnad. Én utfordring med utstrakt bruk av fakes er at du sjelden får testet den ekte implementasjonen av for eksempel HTTP-klienten din. Du kan ende opp med en testsuite som er grønn og fin, men applikasjonen feiler i produksjon på grunn av en bug i selve integrasjonskoden.
For å mitigere dette, bør du holde den faktiske implementasjonen så tynn som mulig. Unngå tung logikk og filtrering i selve integrasjonsklassen – flytt heller dette til domenelogikken eller delte komponenter som også brukes av faken. I tillegg kan man vurdere å ha tester som bruker verktøy som Wiremock for å verifisere at den ekte klienten fungerer korrekt.
En annen risiko er at faken din ikke speiler virkeligheten 100%. Hvis det ekte systemet oppfører seg annerledes enn du har antatt (og implementert i faken), vil du få en falsk trygghet. Samtidig kan man si at så lenge man har gode definerte grensesnitt mellom tjenestene sine, og at oppførselen for ulike scenarier er godt definert vil denne risikoen være mindre.
Til slutt tar det noe tid å skrive og vedlikeholde fakes og i noen tilfeller kan en fake bli ganske kompleks. Men fakes er et verktøy som hører hjemme i enhver utviklers verktøykasse – og verdien det gir i form av utviklingshastighet og testglede vil i i de fleste tilfeller veie opp for kostnaden ved vedlikehold og oppsett.