Dit is deel vijf van de vijf in een zelfstudieserie over het testen van gegevensintensieve code. In deel vier behandelde ik externe datastores, gebruikte gedeelde testdatabases, maakte gebruik van snapshots van productiegegevens en genereerde uw eigen testgegevens. In deze zelfstudie ga ik over fuzz-testen, het testen van uw cache, het testen van gegevensintegriteit, het testen van idempotency en ontbrekende gegevens.
Het idee van fuzz-testen is om het systeem te overweldigen met veel willekeurige invoer. In plaats van te proberen input te bedenken die alle gevallen bestrijkt, die moeilijk en / of zeer arbeidsintensief kunnen zijn, laat je het toeval het voor je doen. Het is conceptueel vergelijkbaar met het genereren van willekeurige gegevens, maar de bedoeling is hier om willekeurige of semi-willekeurige invoer te genereren in plaats van persistente gegevens.
Fuzz-testen is met name handig voor het vinden van beveiligings- en prestatieproblemen wanneer onverwachte ingangen crashes of geheugenlekken veroorzaken. Maar het kan er ook voor zorgen dat alle ongeldige invoer vroegtijdig wordt gedetecteerd en door het systeem op de juiste manier wordt geweigerd.
Denk bijvoorbeeld aan invoer in de vorm van diep geneste JSON-documenten (heel gebruikelijk in web-API's). Proberen om handmatig een uitgebreide lijst met testcases te genereren, is zowel foutgevoelig als veel werk. Maar fuzz testen is de perfecte techniek.
Er zijn verschillende bibliotheken die u kunt gebruiken voor fuzz-testen. Mijn favoriet is gofuzz van Google. Hier is een eenvoudig voorbeeld dat automatisch 200 unieke objecten van een struct genereert met verschillende velden, inclusief een geneste struct.
import ("fmt" "github.com/google/gofuzz") func SimpleFuzzing () type SomeType struct A string B string C int D struct E float64 f: = fuzz.New () object: = SomeType uniqueObjects: = map [SomeType] int voor i: = 0; ik < 200; i++ f.Fuzz(&object) uniqueObjects[object]++ fmt.Printf("Got %v unique objects.\n", len(uniqueObjects)) // Output: // Got 200 unique objects.
Vrijwel elk complex systeem dat met veel gegevens omgaat heeft een cache, of waarschijnlijker meerdere niveaus van hiërarchische caches. Zoals het gezegde luidt, zijn er slechts twee moeilijke dingen in de informatica: dingen benoemen, cache-ongeldigverklaring en uitschakelen door één fout.
Geen grapjes, het beheren van uw cachingstrategie en implementatie kan uw gegevenstoegang bemoeilijken, maar heeft een enorme invloed op uw kosten en prestaties voor gegevenstoegang. Het testen van uw cache kan niet van buitenaf worden gedaan omdat uw interface zich verbergt waar de gegevens vandaan komen en het cachemechanisme een implementatiedetail is.
Laten we eens kijken hoe het cachegedrag van de hybride gegevenslaag van Songify getest kan worden.
Caches leven en sterven door hun hit / miss-prestaties. De basisfunctionaliteit van een cache is dat als aangevraagde gegevens beschikbaar zijn in de cache (een treffer), deze dan wordt opgehaald uit de cache en niet uit de primaire datastore. In het oorspronkelijke ontwerp van de HybridDataLayer
, de toegang tot de cache gebeurde op privé-manieren.
Door regels voor zichtbaarheid te gebruiken, kunt u ze niet rechtstreeks bellen of ze uit een ander pakket vervangen. Om cachetests in te schakelen, zal ik die methoden wijzigen in openbare functies. Dit is prima omdat de daadwerkelijke applicatiecode werkt via de dataLayer
interface, die deze methoden niet blootstelt.
De testcode zal deze openbare functies echter kunnen vervangen als dat nodig is. Laten we eerst een methode toevoegen om toegang te krijgen tot de Redis-client, zodat we de cache kunnen manipuleren:
func (m * HybridDataLayer) GetRedis () * redis.Client return m.redis
Vervolgens zal ik de veranderen getSongByUser_DB ()
methoden voor een openbare functievariabele. Nu, in de test, kan ik de GetSongsByUser_DB ()
variabele met een functie die bijhoudt hoe vaak het werd aangeroepen en vervolgens doorstuurt naar de oorspronkelijke functie. Dat stelt ons in staat om te verifiëren of een oproep naar GetSongsByUser ()
haalde de nummers uit de cache of uit de database.
Laten we het stuk voor stuk opsplitsen. Eerst krijgen we de gegevenslaag (die ook de DB en redis wist), een gebruiker maken en een nummer toevoegen. De AddSong ()
methode geeft ook redis op.
func TestGetSongsByUser_Cache (t * testing.T) nu: = time.Now () u: = Gebruiker Name: "Gigi", Email: "[email protected]", RegisteredAt: now, LastLogin: now dl, err : = getDataLayer () if err! = nil t.Error ("Kan hybride gegevenslaag niet maken") err = dl.CreateUser (u) if err! = nil t.Error ("Kan gebruiker niet maken") lm, err: = NewSongManager (u, dl) if err! = nil t.Error ("NewSongManager ()" nihil "terug") err = lm.AddSong (testSong, nil) if err! = nil t .Error ("AddSong () failed")
Dit is het coole deel. Ik behoud de oorspronkelijke functie en definieer een nieuwe geïnstrumenteerde functie die de lokale verhoogt callCount
variabele (het zit allemaal in een afsluiting) en roept de oorspronkelijke functie aan. Vervolgens wijs ik de geïnstrumenteerde functie toe aan de variabele GetSongsByUser_DB
. Vanaf nu wordt elke aanroep van de hybride gegevenslaag naar GetSongsByUser_DB ()
zal naar de geïnstrumenteerde functie gaan.
callCount: = 0 originalFunc: = GetSongsByUser_DB instrumentedFunc: = func (m * HybridDataLayer, e-mailstring, liedjes * [] Song) (err-fout) callCount + = 1 return originalFunc (m, email, liedjes) GetSongsByUser_DB = instrumentedFunc
Op dit moment zijn we klaar om de werking van de cache daadwerkelijk te testen. Ten eerste noemt de test de GetSongsByUser ()
van de SongManager
die doorstuurt naar de hybride gegevenslaag. De cache zou moeten worden ingevuld voor deze gebruiker die we zojuist hebben toegevoegd. Dus het verwachte resultaat is dat onze geïnstrumenteerde functie niet zal worden aangeroepen, en de callCount
zal op nul blijven.
_, err = lm.GetSongsByUser (u) if err! = nil t.Error ("GetSongsByUser () failed") // Verifieer of de DB niet is benaderd, want cache moet // worden ingevuld door AddSong () als callCount > 0 t.Error ('GetSongsByUser_DB () called when it should not have')
De laatste testcase is om ervoor te zorgen dat als de gegevens van de gebruiker niet in de cache staan, deze correct wordt opgehaald uit de database. De test volbrengt het door Redis weg te spoelen (alle gegevens wissen) en een nieuwe oproep te doen GetSongsByUser ()
. Deze keer zal de geïnstrumenteerde functie worden aangeroepen, en de test verifieert dat de callCount
is gelijk aan 1. Eindelijk, het origineel GetSongsByUser_DB ()
functie is hersteld.
// Wis de cache dl.GetRedis (). FlushDB () // Haal de nummers opnieuw op, nu moet het naar de database gaan // omdat de cache leeg is _, err = lm.GetSongsByUser (u) if err! = Nil t.Error ("GetSongsByUser () failed") // Controleer of de DB is benaderd omdat de cache leeg is als callCount! = 1 t.Error ('GetSongsByUser_DB () is niet een keer gebeld zoals zou moeten zijn') GetSongsByUser_DB = originalFunc
Onze cache is erg eenvoudig en doet geen enkele invalidatie. Dit werkt vrij goed zolang alle nummers zijn toegevoegd via de AddSong ()
methode die zorgt voor het updaten van Redis. Als we meer bewerkingen toevoegen, zoals het verwijderen van nummers of het verwijderen van gebruikers, moeten deze bewerkingen ervoor zorgen dat Redis dienovereenkomstig wordt bijgewerkt.
Deze zeer eenvoudige cache werkt ook als we een gedistribueerd systeem hebben waarop meerdere onafhankelijke machines onze Songify-service kunnen uitvoeren, zolang alle instanties met dezelfde DB- en Redis-instanties werken.
Als de DB en de cache echter niet meer synchroon lopen vanwege onderhoudswerkzaamheden of andere hulpprogramma's en toepassingen die onze gegevens wijzigen, moeten we een invalidatie- en vernieuwingsbeleid voor de cache opstellen. Het kan worden getest met dezelfde technieken: vervang doelfuncties of rechtstreeks toegang tot de DB en Redis in uw test om de status te verifiëren.
Meestal kun je de cache niet zomaar oneindig laten groeien. Een veelgebruikt schema om de meest bruikbare gegevens in de cache te bewaren, zijn LRU-caches (het minst recentelijk gebruikt). De oudste gegevens worden uit de cache gehaald wanneer deze de capaciteit bereikt.
Het testen ervan houdt in dat de capaciteit tijdens de test op een relatief klein aantal wordt ingesteld, dat de capaciteit wordt overschreden en dat de oudste gegevens niet meer in de cache worden bewaard en daarvoor toegang tot DB vereist is.
Uw systeem is slechts zo goed als uw gegevensintegriteit. Als je gegevens hebt beschadigd of gegevens hebt gemist, ben je in slechte staat. In real-world systemen is het moeilijk om een perfecte gegevensintegriteit te behouden. Schema en indelingen veranderen, gegevens worden opgenomen via kanalen die mogelijk niet controleren op alle beperkingen, fouten laten slechte gegevens in, beheerders proberen handmatige fixes uit te voeren, back-ups en herstelbewerkingen kunnen onbetrouwbaar zijn.
Gezien deze harde realiteit, moet u de gegevensintegriteit van uw systeem testen. Het testen van gegevensintegriteit is anders dan reguliere geautomatiseerde tests na elke codeverandering. De reden is dat gegevens slecht kunnen gaan, zelfs als de code niet is gewijzigd. U wilt absoluut gegevensintegriteitscontroles uitvoeren na codewijzigingen die de gegevensopslag of -representatie kunnen veranderen, maar ze ook periodiek kunnen uitvoeren.
Beperkingen vormen de basis van uw datamodellering. Als u een relationele database gebruikt, kunt u enkele beperkingen op SQL-niveau definiëren en deze door DB laten afdwingen. Nullness, lengte van tekstvelden, uniciteit en 1-N relaties kunnen eenvoudig worden gedefinieerd. SQL kan echter niet alle beperkingen controleren.
In Desongcious is er bijvoorbeeld een N-N-relatie tussen gebruikers en liedjes. Elk nummer moet aan minimaal één gebruiker zijn gekoppeld. Er is geen goede manier om dit in SQL af te dwingen (nou ja, je kunt een foreign key van song naar user hebben en de song naar een van de gebruikers laten verwijzen). Een andere beperking kan zijn dat elke gebruiker maximaal 500 nummers heeft. Nogmaals, er is geen manier om het in SQL te vertegenwoordigen. Als u NoSQL-gegevensopslag gebruikt, is er meestal nog minder ondersteuning voor het aangeven en valideren van beperkingen op het niveau van gegevensopslag.
Dat laat je met een paar opties:
Idempotency betekent dat dezelfde bewerking meerdere keren achter elkaar hetzelfde effect zal hebben als het één keer uitvoeren.
Het instellen van de variabele x tot 5 is bijvoorbeeld idempotent. Je kunt x tot 5 een keer of een miljoen keer instellen. Het zal nog steeds 5 zijn. Het is echter niet idempotent om X met 1 te verhogen. Elke opeenvolgende verhoging wijzigt de waarde ervan. Idempotency is een zeer wenselijke eigenschap in gedistribueerde systemen met tijdelijke netwerkpartities en herstelprotocollen die opnieuw proberen een bericht meerdere keren te verzenden als er geen onmiddellijk antwoord is.
Als u idempotency in uw datacommercode ontwerpt, zou u het moeten testen. Dit is meestal heel gemakkelijk. Voor elke idempotente bewerking verlengt u om de bewerking tweemaal of meer achter elkaar uit te voeren en te controleren of er geen fouten zijn en blijft de status hetzelfde.
Merk op dat idempotent ontwerp soms fouten kan verbergen. Overweeg een record uit een database te verwijderen. Het is een idempotente operatie. Nadat u een record hebt verwijderd, bestaat het record niet meer in het systeem en als u het opnieuw probeert te verwijderen, wordt het niet meer teruggehaald. Dat betekent dat een poging om een niet-bestaande record te verwijderen een geldige bewerking is. Maar het kan het feit maskeren dat de beller de verkeerde recordsleutel heeft gepasseerd. Als je een foutmelding geeft, is het niet idempotent.
Datamigraties kunnen zeer risicovolle operaties zijn. Soms voer je een script uit over al je gegevens of kritieke delen van je gegevens en voer je een serieuze operatie uit. Je moet klaar zijn met plan B voor het geval er iets misgaat (ga bijvoorbeeld terug naar de oorspronkelijke gegevens en zoek uit wat er mis is gegaan).
In veel gevallen kan datamigratie een trage en kostbare operatie zijn waarvoor twee systemen naast elkaar nodig zijn voor de duur van de migratie. Ik heb deelgenomen aan verschillende datamigraties die enkele dagen of zelfs weken duurden. Wanneer u geconfronteerd wordt met een enorme datamigratie, is het de moeite waard om de tijd te investeren en de migratie zelf te testen op een kleine (maar representatieve) subset van uw gegevens en vervolgens te controleren of de nieuw gemigreerde gegevens geldig zijn en het systeem ermee kan werken.
Ontbrekende gegevens vormen een interessant probleem. Soms zullen ontbrekende gegevens uw gegevensintegriteit schenden (bijvoorbeeld een nummer waarvan de gebruiker ontbreekt) en soms ontbreekt het (bijvoorbeeld iemand verwijdert een gebruiker en alle nummers).
Als de ontbrekende gegevens een probleem met de gegevensintegriteit veroorzaken, zult u het in uw gegevensintegriteitstests detecteren. Als sommige gegevens echter gewoon ontbreken, is er geen eenvoudige manier om deze te detecteren. Als de gegevens nooit in een permanente opslag zijn terechtgekomen, is er misschien een spoor in de logboeken of andere tijdelijke winkels.
Afhankelijk van hoeveel van het risico ontbrekende gegevens zijn, kunt u enkele tests schrijven die opzettelijk sommige gegevens van uw systeem verwijderen en controleren of het systeem zich gedraagt zoals verwacht.
Het testen van gegevensintensieve code vereist een doelbewuste planning en een goed begrip van uw kwaliteitseisen. U kunt testen op verschillende niveaus van abstractie, en uw keuzes zullen van invloed zijn op hoe grondig en uitgebreid uw tests zijn, hoeveel aspecten van uw werkelijke gegevenslaag u test, hoe snel uw tests worden uitgevoerd en hoe gemakkelijk het is om uw tests aan te passen wanneer wijzigingen in de gegevenslaag.
Er is geen enkel goed antwoord. U moet uw favoriete plekje vinden, van superomvattende, langzame en arbeidsintensieve tests tot snelle, lichtgewicht tests.