Testing av dataintensiv kode med Go, del 5

Oversikt

Dette er del fem av fem i en opplæringsserie om testing av dataintensiv kode. I del fire har jeg dekket eksterne datalager, ved hjelp av delte testdatabaser, ved hjelp av øyeblikksbilder av produksjonsdata, og genererer dine egne testdata. I denne veiledningen vil jeg gå over fuzz testing, teste cachen din, teste data integritet, teste idempotency og manglende data.

Fuzz Testing

Ideen om fuzz testing er å overvelde systemet med mye tilfeldig inngang. I stedet for å prøve å tenke på input som vil dekke alle tilfeller, som kan være vanskelig og / eller svært arbeidsintensiv, lar du sjansen gjøre det for deg. Det er konseptuelt lik tilfeldig datagenerering, men hensikten her er å generere tilfeldige eller semi-tilfeldige innganger i stedet for vedvarende data.

Når er Fuzz Testing Nyttig?

Fuzz-testing er spesielt nyttig for å finne sikkerhets- og ytelsesproblemer når uventede innganger forårsaker krasjer eller minnelekkasjer. Men det kan også bidra til at alle ugyldige innganger blir oppdaget tidlig og avvises riktig av systemet.

Tenk for eksempel innspill som kommer i form av dypt nestede JSON-dokumenter (svært vanlig i web-APIer). Å prøve å generere en omfattende liste over testtilfeller manuelt, er både feilaktig og mye arbeid. Men fuzz testing er den perfekte teknikken.

Bruke Fuzz Testing 

Det finnes flere biblioteker du kan bruke til fuzz-testing. Min favoritt er gofuzz ​​fra Google. Her er et enkelt eksempel som automatisk genererer 200 unike objekter av en struktur med flere felt, inkludert en nestet struktur.  

importere ("fmt" "github.com/google/gofuzz") func SimpleFuzzing () type SomeType struct En streng B streng C int D struct E float64 f: = fuzz.New () objekt: = SomeType   uniqueObjects: = map [SomeType] int  for i: = 0; Jeg < 200; i++  f.Fuzz(&object) uniqueObjects[object]++  fmt.Printf("Got %v unique objects.\n", len(uniqueObjects)) // Output: // Got 200 unique objects.  

Testing av cachen din

Nesten alle komplekse systemer som håndterer mye data har en cache, eller mer sannsynlig flere nivåer av hierarkiske caches. Som det sier, er det bare to vanskelige ting i datavitenskap: navngi ting, cache ugyldighet og av en feil.

Vitser til side, styring av cachingstrategi og implementering kan komplisere datatilgangen, men har en enorm innvirkning på datatilgangskostnad og ytelse. Testing av cachen din kan ikke gjøres fra utsiden fordi grensesnittet ditt skjuler hvor dataene kommer fra, og cachemekanismen er en implementeringsdetalj.

La oss se hvordan du tester cacheadferdigheten til Songify-hybriddatagelet.

Cache Hits og Misses

Caches lever og dør av deres hit / miss ytelse. Den grunnleggende funksjonaliteten til en cache er at hvis ønsket data er tilgjengelig i hurtigbufferen (et treff), blir det hentet fra hurtigbufferen og ikke fra den primære datalageret. I den opprinnelige utformingen av HybridDataLayer, Cache-tilgangen ble gjort gjennom private metoder.

Go-synlighetsregler gjør det umulig å ringe dem direkte eller erstatte dem fra en annen pakke. For å aktivere cachertesting, endrer jeg disse metodene til offentlige funksjoner. Dette er greit fordi den faktiske applikasjonskoden går gjennom layer grensesnitt, som ikke avslører disse metodene.

Testkoden vil imidlertid kunne erstatte disse offentlige funksjonene etter behov. La oss først legge til en metode for å få tilgang til Redis-klienten, slik at vi kan manipulere hurtigbufferen:

func (m * HybridDataLayer) GetRedis () * redis.Client return m.redis 

Neste endrer jeg getSongByUser_DB () metoder til en offentlig funksjonsvariabel. Nå, i testen, kan jeg erstatte GetSongsByUser_DB () variabel med en funksjon som holder styr på hvor mange ganger den ble kalt og deretter videresender den til den opprinnelige funksjonen. Det tillater oss å verifisere om en samtale til GetSongsByUser () hentet sangene fra hurtigbufferen eller fra DB. 

La oss slå det ned stykke for bit. Først får vi datalaget (som også fjerner DB og redis), lager en bruker, og legger til en sang. De AddSong () Metoden fyller også redis. 

func TestGetSongsByUser_Cache (t * testing.T) nå: = time.Now () u: = Bruker Navn: "Gigi", E-post: "[email protected]", RegisteredAt: nå, LastLogin: nå dl, err : = getDataLayer () hvis err! = null t.Error ("Kunne ikke opprette hybrid datalag") err = dl.CreateUser (u) hvis feil! = null t.Error ("Kunne ikke opprette bruker")  lm, err: = NewSongManager (u, dl) hvis feil! = null t.Error ("NewSongManager () returnert" nil ") err = lm.AddSong (testSong, null) hvis feil! = null t .Error ("AddSong () mislyktes") 

Dette er den kule delen. Jeg beholder den opprinnelige funksjonen og definerer en ny instrumentert funksjon som øker det lokale callCount variabel (det er alt i et lukket) og kaller den opprinnelige funksjonen. Deretter tilordner jeg den instrumenterte funksjonen til variabelen GetSongsByUser_DB. Fra nå av, hver samtale fra hybriddatalaget til GetSongsByUser_DB () vil gå til den instrumenterte funksjonen.     

 callCount: = 0 originalFunc: = GetSongsByUser_DB instrumentedFunc: = func (m * HybridDataLayer, e-post streng, sanger * [] Sang) (feil feil) callCount + = 1 returner originalFunc (m, e-post, sanger) GetSongsByUser_DB = instrumentedFunc 

På dette tidspunktet er vi klare til å teste hurtigbufferoperasjonen. For det første kaller testen GetSongsByUser () av SongManager som videresender den til hybriddatalaget. Cachen skal fylles for denne brukeren vi nettopp har lagt til. Så det forventede resultatet er at vår instrumenterte funksjon ikke vil bli kalt, og callCount vil forbli null.

 _, err = lm.GetSongsByUser (u) hvis feil! = null t.Error ("GetSongsByUser () mislyktes") // Bekreft at DB ikke ble åpnet fordi cache skulle være // befolket av AddSong () hvis callCount > 0 t.Error ('GetSongsByUser_DB () kalt når den ikke burde ha') 

Det siste testet tilfelle er å sikre at hvis brukerens data ikke er i hurtigbufferen, blir den hentet riktig fra DB. Testen oppnår det ved å skylle Redis (rydde alle dataene) og ringe til GetSongsByUser (). Denne gangen vil den instrumenterte funksjonen bli kalt, og testen verifiserer at callCount er lik 1. Endelig, den opprinnelige GetSongsByUser_DB () funksjonen er gjenopprettet.

 // Slett cachen dl.GetRedis (). FlushDB () // Få sangene igjen, nå skal det gå til DB // fordi hurtigbufferen er tom _, err = lm.GetSongsByUser (u) hvis feil! = Null t.Error ("GetSongsByUser () mislyktes") // Bekreft at DB var tilgjengelig fordi hurtigbufferen er tom hvis callCount! = 1 t.Error ('GetSongsByUser_DB () ikke ble kalt en gang som den skulle ha')  GetSongsByUser_DB = originalFunc

Cache Invalidation

Cachen vår er veldig enkel og gjør ingen ugyldighet. Dette fungerer ganske bra så lenge alle sangene blir lagt til gjennom AddSong () metode som tar seg av oppdatering av Redis. Hvis vi legger til flere operasjoner som å fjerne sanger eller slette brukere, bør disse operasjonene sørge for at Redis oppdateres tilsvarende.

Denne svært enkle hurtigbufferen vil fungere selv om vi har et distribuert system der flere uavhengige maskiner kan kjøre vår Songify-tjeneste, så lenge alle forekomster fungerer med de samme DB- og Redis-forekomstene.

Men hvis DB og cache kan komme seg ut av synkronisering på grunn av vedlikeholdsoperasjoner eller andre verktøy og applikasjoner som endrer dataene våre, må vi opprette en ugyldighets- og oppdateringspolicy for hurtigbufferen. Det kan testes ved hjelp av samme teknikker - erstatte målfunksjoner eller direkte tilgang til DB og Redis i testen for å verifisere staten.

LRU Caches

Vanligvis kan du ikke bare la hurtigbufferen vokse uendelig. Et vanlig system for å holde de mest nyttige dataene i hurtigbufferen er LRU-cacher (minst nylig brukt). De eldste dataene blir rammet fra hurtigbufferen når den når kapasitet.

Testing innebærer å sette kapasiteten til et relativt lite antall under testen, som overskrider kapasiteten, og sikrer at de eldste dataene ikke er i hurtigbufferen lenger, og å få tilgang til den krever DB-tilgang. 

Teste dataintegriteten din

Systemet ditt er bare like godt som dataintegriteten din. Hvis du har ødelagt data eller mangler data, er du i dårlig form. I virkelige systemer er det vanskelig å opprettholde perfekt dataintegritet. Skjema og formater endres, data blir inntatt gjennom kanaler som kanskje ikke kontrollerer alle begrensningene, feilene gir dårlig data, administratorer forsøker manuelle reparasjoner, sikkerhetskopier og gjenoppretting kan være upålitelige.

Gitt denne harde virkeligheten, bør du teste systemets dataintegritet. Testing av dataintegritet er forskjellig fra vanlige automatiserte tester etter hver kodeendring. Årsaken er at data kan gå dårlig selv om koden ikke endret seg. Du vil helt sikkert kjøre dataintegritetskontroller etter kodeendringer som kan endre datalagring eller representasjon, men også kjøre dem med jevne mellomrom.

Testing av begrensninger

Begrensninger er grunnlaget for datamodellen din. Hvis du bruker en relasjons DB, kan du definere noen begrensninger på SQL-nivået og la DB håndheve dem. Nullness, lengden på tekstfelt, unikt og 1-N-forhold kan enkelt defineres. Men SQL kan ikke kontrollere alle begrensningene.

For eksempel, i Desongcious, er det et N-N forhold mellom brukere og sanger. Hver sang må være tilknyttet minst en bruker. Det er ingen god måte å håndheve dette på i SQL (vel, du kan ha en fremmednøkkel fra sang til bruker og få sangpunktet til en av brukerne som er tilknyttet den). En annen begrensning kan være at hver bruker kan ha maksimalt 500 sanger. Igjen, det er ingen måte å representere det i SQL. Hvis du bruker NoSQL datalager, så er det vanligvis enda mindre støtte for å deklarere og validere begrensninger på datalagernivå.

Det gir deg et par alternativer:

  • Sørg for at tilgang til data kun går gjennom vettede grensesnitt og verktøy som håndhever alle begrensningene.
  • Skann dataene dine jevnlig, jager begrensninger og reparer dem.    

Testing Idempotency

Idempotency betyr at å utføre samme operasjon flere ganger på rad, vil ha samme effekt som å utføre det en gang. 

For eksempel er innstillingen av variabelen x til 5 idempotent. Du kan angi x til 5 en gang eller en million ganger. Det vil fortsatt være 5. Imidlertid er økning av X ved 1 ikke idempotent. Hver sammenhengende stigning endrer verdien. Idempotency er en svært ønskelig egenskap i distribuerte systemer med midlertidige nettverkspartisjoner og gjenoppretting protokoller som forsøker å sende en melding flere ganger hvis det ikke er umiddelbar respons.

Hvis du utformer idempotency i dataadgangskoden din, bør du teste den. Dette er vanligvis veldig enkelt. For hver idempotent operasjon strekker du ut for å utføre operasjonen to eller flere på rad og verifisere at det ikke er feil og staten forblir den samme.   

Merk at idempotent design noen ganger kan skjule feil. Vurder å slette en post fra en DB. Det er en idempotent operasjon. Etter at du har slettet en plate, eksisterer ikke posten i systemet lenger, og forsøk på å slette den igjen, vil ikke ta den tilbake. Det betyr at forsøk på å slette en ikke-eksisterende post er en gyldig operasjon. Men det kan skjule det faktum at feil rekordnøkkel ble passert av den som ringer. Hvis du returnerer en feilmelding, er den ikke idempotent.    

Testing av dataoverføringer

Dataoverføringer kan være svært risikable operasjoner. Noen ganger kjører du et skript over alle dataene dine eller kritiske deler av dataene dine og utfører noen alvorlige operasjoner. Du bør være klar med plan B hvis noe går galt (for eksempel gå tilbake til de opprinnelige dataene og finn ut hva som gikk galt).

I mange tilfeller kan dataoverføring være en sakte og kostbar operasjon som kan kreve to systemer side om side i løpet av overføringen. Jeg deltok i flere dataoverføringer som tok flere dager eller til og med uker. Når du møter en massiv dataoverføring, er det verdt å investere tiden og teste migrasjonen selv på en liten (men representativ) delmengde av dataene dine og deretter bekrefte at de nylig migrerte dataene er gyldige og systemet kan fungere med det. 

Testing av manglende data

Manglende data er et interessant problem. Noen ganger mangler data brudd på dataintegriteten din (for eksempel en sang hvis bruker mangler), og noen ganger mangler det bare (for eksempel fjerner noen bruker og alle sangene sine).

Hvis de manglende dataene forårsaker et dataintegritetproblem, vil du oppdage det i dataintegritetestene dine. Men hvis noen data bare mangler, er det ingen enkel måte å oppdage. Hvis dataene aldri gjorde det til vedvarende lagring, så er det kanskje spor i loggene eller andre midlertidige butikker.

Avhengig av hvor mye av en risiko som mangler data, kan du skrive noen tester som bevisst fjerner noen data fra systemet ditt og kontrollere at systemet oppfører seg som forventet.

Konklusjon

Testing av dataintensiv kode krever bevisst planlegging og forståelse av dine kvalitetskrav. Du kan teste på flere nivåer av abstraksjon, og valgene dine vil påvirke hvor grundig og omfattende testene dine er, hvor mange aspekter av ditt faktiske datalag du tester, hvor fort testene dine går, og hvor enkelt det er å endre testene når data lag endres.

Det er ikke noe enkelt korrekt svar. Du må finne din søte flekk langs spekteret fra super omfattende, sakte og arbeidskrevende tester til raske, lette tester.