Testing av dataintensiv kod med go, del 5

Översikt

Detta är del fem av fem i en handledningsserie om testning av dataintensiv kod. I del fyra täckte jag fjärranslutna datalager, med hjälp av delade testdatabaser, med hjälp av snapshots för produktionsdata och generering av egna testdata. I denna handledning går jag över fuzz-testning, testar din cache, testar dataintegritet, testar idempotency och saknar data.

Fuzz-testning

Tanken med fuzz-test är att överväldiga systemet med massor av slumpmässig inmatning. Istället för att försöka tänka på insatser som täcker alla fall, vilket kan vara svårt och / eller mycket arbetsintensivt, låter du chansen göra det åt dig. Det är begreppsmässigt lik slumpmässig datagenerering, men avsikten är här att generera slumpmässiga eller halvlösa inmatningar i stället för ihållande data.

När är Fuzz-testning användbar?

Fuzz-testning är speciellt användbar för att hitta säkerhets- och prestandaproblem när oväntade ingångar orsakar kraschar eller minnesläckor. Men det kan också bidra till att alla ogiltiga inmatningar upptäcks tidigt och avvisas ordentligt av systemet.

Tänk på, till exempel, inmatning som kommer i form av djupt kapslade JSON-dokument (mycket vanligt i web API). Att försöka generera manuellt en omfattande lista över testfall är både felaktigt och mycket arbete. Men fuzzprovning är den perfekta tekniken.

Använda Fuzz-testning 

Det finns flera bibliotek du kan använda för fuzz-testning. Min favorit är gofuzz ​​från Google. Här är ett enkelt exempel som automatiskt genererar 200 unika objekt i en struktur med flera fält, inklusive en kapslad struktur.  

importera ("fmt" "github.com/google/gofuzz") func SimpleFuzzing () typ SomeType struct En sträng B-sträng C int D struktur E float64 f: = fuzz.New () objekt: = SomeType   uniqueObjects: = map [SomeType] int  för i: = 0; jag < 200; i++  f.Fuzz(&object) uniqueObjects[object]++  fmt.Printf("Got %v unique objects.\n", len(uniqueObjects)) // Output: // Got 200 unique objects.  

Testa din cache

Nästan alla komplexa system som hanterar mycket data har en cache, eller mer sannolikt flera nivåer av hierarkiska cacher. Som sagt säger det sig att det bara finns två svåra saker i datavetenskap: namngivning, felaktig cache och av en fel.

Skämt åt sidan, hantera din cachingstrategi och implementering kan komplicera din dataåtkomst men har en enorm inverkan på din datakommunikationskostnad och prestanda. Att testa din cache kan inte göras från utsidan eftersom ditt gränssnitt döljer var data kommer ifrån och cachemekanismen är en implementeringsdetalj.

Låt oss se hur du testar cache-beteendet hos Songify-hybriddatalaget.

Cache Hits och Misses

Caches lever och dör av deras hit / miss prestanda. Den grundläggande funktionaliteten för en cache är att om begärd data finns tillgänglig i cachen (en träff) kommer den hämtas från cacheminnet och inte från den primära datalagen. I den ursprungliga designen av HybridDataLayer, cachetillgången gjordes genom privata metoder.

Gå siktregler gör det omöjligt att ringa dem direkt eller ersätta dem från ett annat paket. För att aktivera cachertestning ändrar jag dessa metoder till offentliga funktioner. Det här är bra eftersom den verkliga applikationskoden fungerar via datalayer gränssnitt, som inte exponerar dessa metoder.

Testkoden kommer emellertid att kunna ersätta dessa offentliga funktioner efter behov. Låt oss först lägga till en metod för att få tillgång till Redis-klienten, så vi kan manipulera cacheminnet:

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

Nästa ska jag ändra getSongByUser_DB () metoder för en offentlig funktion variabel. Nu kan jag i provet ersätta GetSongsByUser_DB () variabel med en funktion som håller reda på hur många gånger det heter och sedan vidarebefordrar det till den ursprungliga funktionen. Det tillåter oss att verifiera om ett samtal till GetSongsByUser () hämtade låtarna från cacheminnet eller från DB. 

Låt oss bryta ner det stycket för bit. Först får vi datalagret (som också rensar DB och redis), skapar en användare och lägger till en låt. De AddSong () Metoden fyller också redis. 

func TestGetSongsByUser_Cache (t * test.T) nu: = time.Now () u: = Användare Namn: "Gigi", Email: "[email protected]", RegisteredAt: now, LastLogin: now dl, err : = getDataLayer () om err! = nil t.Error ("Misslyckades med att skapa hybriddataskikt") err = dl.CreateUser (u) om err! = nil t.Error ("Misslyckades med att skapa användare")  lm, err: = NewSongManager (u, dl) om err! = nil t.Error ("NewSongManager () returneras" nil ") err = lm.AddSong (testSong, noll) om err! = nil t .Error ("AddSong () misslyckades") 

Det här är den snygga delen. Jag behåller den ursprungliga funktionen och definierar en ny instrumentfunktion som ökar lokaliteten callCount variabel (det är allt i en stängning) och kallar den ursprungliga funktionen. Sedan tilldelar jag den instrumenterade funktionen till variabeln GetSongsByUser_DB. Från och med nu, varje samtal från hybriddataskiktet till GetSongsByUser_DB () kommer att gå till den instrumenterade funktionen.     

 callCount: = 0 originalFunc: = GetSongsByUser_DB instrumentedFunc: = func (m * HybridDataLayer, e-poststräng, låtar * [] Song) (felmeddelande) callCount + = 1 returnera originalFunc (m, email, songs) GetSongsByUser_DB = instrumentedFunc 

Vid denna tidpunkt är vi redo att faktiskt testa cachoperationen. Först kallar testet GetSongsByUser () av SongManager som vidarebefordrar det till hybriddataskiktet. Cachen ska fyllas i för den användaren som vi just lagt till. Så det förväntade resultatet är att vår instrumenterade funktion inte kommer att kallas, och callCount kommer att förbli noll.

 _, err = lm.GetSongsByUser (u) om err! = nil t.Error ("GetSongsByUser () misslyckades") // Verifiera att DB inte kunde nås eftersom cachen skulle vara // populerad av AddSong () om callCount > 0 t.Error ('GetSongsByUser_DB () kallas när den inte borde') 

Det sista testfallet är att se till att om användarens data inte finns i cachen kommer den att hämtas korrekt från DB. Testet fullbordar det genom att spola Redis (rensa all dess data) och ringa till ett annat samtal GetSongsByUser (). Denna gång kommer den instrumenterade funktionen att ringas, och testet verifierar att callCount är lika med 1. Slutligen, originalet GetSongsByUser_DB () funktionen återställs.

 // Rensa cachen dl.GetRedis (). FlushDB () // Hämta låtarna igen, nu ska det gå till DB // eftersom cachen är tom _, err = lm.GetSongsByUser (u) om err! = Nil t.Error ("GetSongsByUser () misslyckades") // Verifiera DB var åtkomlig eftersom cachen är tom om callCount! = 1 t.Error ('GetSongsByUser_DB () inte kallades en gång som den borde ha')  GetSongsByUser_DB = originalFunc

Cache Invalidation

Vår cache är väldigt grundläggande och gör ingen ogiltigförklaring. Det fungerar ganska bra så länge som alla låtar läggs till via AddSong () metod som tar hand om uppdatering av Redis. Om vi ​​lägger till fler funktioner som att ta bort låtar eller radera användare, bör dessa åtgärder ta hand om uppdatering av Redis i enlighet med detta.

Denna mycket enkla cache fungerar även om vi har ett distribuerat system där flera oberoende maskiner kan köra vår Songify-tjänst, så länge som alla fall fungerar med samma DB och Redis-instanser.

Men om DB och cacheminne kan komma ur synkronisering på grund av underhållsoperationer eller andra verktyg och applikationer som ändrar våra data måste vi göra en invalidiserings- och uppdateringspolicy för cacheminnet. Den kan testas med samma tekniker - ersätta målfunktioner eller direkt åtkomst till DB och Redis i ditt test för att verifiera tillståndet.

LRU Caches

Vanligtvis kan du inte bara låta cachen växa oändligt. Ett vanligt system för att hålla de mest användbara uppgifterna i cacheminnet är LRU-cachor (senast används). De äldsta uppgifterna störas från cacheminnet när det når kapacitet.

Testa det innebär att man ställer in kapacitet till ett relativt litet antal under testet, överstiger kapaciteten och ser till att de äldsta data inte finns i cacheminnet längre och att åtkomst till den kräver DB-åtkomst. 

Testa din dataintegritet

Ditt system är bara lika bra som din dataintegritet. Om du har skadat data eller saknade data då är du i dålig form. I verkliga system är det svårt att upprätthålla perfekt dataintegritet. Schema och format ändras, data tas in via kanaler som kanske inte kontrollerar alla begränsningar, buggar släpper in dåliga data, administratörer försöker manuella korrigeringar, säkerhetskopior och återställningar kan vara opålitliga.

Med tanke på denna hårda verklighet bör du testa ditt systems dataintegritet. Testa dataintegritet är annorlunda än vanliga automatiska tester efter varje kodändring. Anledningen är att data kan bli dåligt även om koden inte ändras. Du vill definitivt köra dataintegritetskontroller efter kodändringar som kan ändra datalagring eller representation, men även köra dem regelbundet.

Testningsbegränsningar

Begränsningar är grunden för din datamodellering. Om du använder en relationell DB kan du definiera vissa begränsningar på SQL-nivån och låta DB genomdriva dem. Nullness, längd på textfält, unika och 1-N-relationer kan enkelt definieras. Men SQL kan inte kontrollera alla begränsningar.

Till exempel, i Desongcious finns det ett N-N-förhållande mellan användare och sånger. Varje låt måste vara associerad med minst en användare. Det finns inget bra sätt att genomdriva detta i SQL (du kan ha en främmande nyckel från låt till användare och låta peka på en av användarna som är associerade med den). En annan begränsning kan vara att varje användare får ha högst 500 låtar. Återigen finns det inget sätt att representera det i SQL. Om du använder NoSQL datalaggar är det vanligtvis ännu mindre stöd för att deklarera och validera begränsningar på datalagringsnivån.

Det ger dig ett par alternativ:

  • Se till att tillgång till data endast går genom vetted gränssnitt och verktyg som verkställer alla begränsningar.
  • Skanna dina data regelbundet, jaktbegränsningar och fixa dem.    

Testa Idempotency

Idempotency innebär att utföra samma operation flera gånger i rad har samma effekt som att utföra den en gång. 

Till exempel är inställningen av variabeln x till 5 idempotent. Du kan ställa in x till 5 en gång eller en miljon gånger. Det kommer fortfarande att vara 5. Men ökningen av X med 1 är inte idempotent. Varje på varandra följande inkrement ändrar sitt värde. Idempotency är en mycket önskvärd egenskap i distribuerade system med temporära nätverkspartitioner och återställningsprotokoll som försöker skicka ett meddelande flera gånger om det inte finns något omedelbart svar.

Om du utformar idempotency i din dataåtkomstkod ska du testa det. Det här är vanligtvis mycket enkelt. För varje idempotent operation sträcker du ut för att utföra operationen två eller flera i rad och verifiera att det inte finns några fel och staten förblir densamma.   

Observera att idempotent design ibland kan dölja fel. Överväg att radera en post från ett DB. Det är en idempotent operation. När du har raderat en post finns inte posten längre i systemet, och försöker att radera den igen kommer inte att ta tillbaka den igen. Det innebär att försök att radera en obefintlig post är en giltig operation. Men det kan maskera det faktum att den felaktiga registreringsnyckeln skickades av den som ringer. Om du returnerar ett felmeddelande är det inte idempotent.    

Testning av dataöverföringar

Data migreringar kan vara mycket riskabla operationer. Ibland kör du ett skript över alla dina data eller kritiska delar av dina data och utför en seriös operation. Du bör vara redo med plan B om något går fel (t ex gå tillbaka till de ursprungliga uppgifterna och ta reda på vad som gick fel).

I många fall kan migrering av data vara en långsam och kostnadseffektiv operation som kan kräva två system sida vid sida under hela migreringen. Jag deltog i flera data migreringar som tog flera dagar eller till och med veckor. När det gäller en enorm data migration är det värt att investera tiden och testa migreringen själv på en liten (men representativ) delmängd av dina data och sedan verifiera att den nyligen migrerade data är giltig och systemet kan fungera med det. 

Testning av saknade data

Saknade data är ett intressant problem. Ibland misslyckas data som bryter mot din dataintegritet (t ex en låt vars användare saknas) och ibland saknas det bara (till exempel tar någon bort en användare och alla deras låtar).

Om de saknade uppgifterna orsakar ett problem med dataintegritet så upptäcker du det i dina dataintegritetstest. Om det emellertid bara saknas några data är det inte enkelt att upptäcka det. Om uppgifterna aldrig gjort det i bestående lagring kanske det finns spår i loggarna eller andra tillfälliga butiker.

Beroende på hur mycket risken saknas data kan du skriva några tester som avsiktligt tar bort vissa data från ditt system och verifiera att systemet beter sig som förväntat.

Slutsats

Testa datakrävande kod kräver avsiktlig planering och förståelse för dina kvalitetskrav. Du kan testa på flera nivåer av abstraktion, och dina val kommer att påverka hur noggrann och omfattande dina tester är, hur många aspekter av ditt faktiska datalager du testar, hur fort testen körs och hur lätt det är att ändra dina test när Dataskiktet ändras.

Det finns inget enda korrekt svar. Du behöver hitta din söta plats längs spektret från super omfattande, långsamma och arbetsintensiva tester till snabba, lätta tester.