Veel niet-triviale systemen zijn ook gegevensintensief of gegevensgestuurd. Het testen van de onderdelen van de systemen die gegevensintensief zijn, is heel anders dan het testen van code-intensieve systemen. Ten eerste kan er veel verfijning in de gegevenslaag zelf zijn, zoals hybride gegevensopslag, caching, back-up en redundantie.
Al deze machines hebben niets met de toepassing zelf te maken, maar moeten worden getest. Ten tweede kan de code erg algemeen zijn en om deze te testen, moet u gegevens genereren die op een bepaalde manier zijn gestructureerd. In deze reeks van vijf tutorials zal ik al deze aspecten bespreken, verschillende strategieën verkennen voor het ontwerpen van testbare data-intensieve systemen met Go, en in specifieke voorbeelden duiken..
In deel een ga ik in op het ontwerp van een abstracte gegevenslaag die goed testen mogelijk maakt, hoe foutafhandeling in de gegevenslaag wordt gedaan, hoe de toegangscode voor gegevens wordt bespot en hoe te testen tegen een abstracte gegevenslaag.
Omgaan met echte datastores en hun fijne kneepjes is gecompliceerd en niet gerelateerd aan de bedrijfslogica. Met het concept van een gegevenslaag kunt u een nette interface aan uw gegevens blootstellen en de bloederige details verbergen over hoe de gegevens precies zijn opgeslagen en hoe u deze kunt openen. Ik gebruik een voorbeeldtoepassing genaamd "Songify" voor persoonlijk muziekbeheer om de concepten met echte code te illustreren.
Laten we het persoonlijke domein voor muziekbeheer opnieuw bekijken - gebruikers kunnen nummers toevoegen en van labels voorzien - en bekijken welke gegevens we moeten opslaan en hoe we deze kunnen openen. De objecten in ons domein zijn gebruikers, nummers en labels. Er zijn twee categorieën bewerkingen die u wilt uitvoeren voor alle gegevens: query's (alleen-lezen) en statuswijzigingen (maken, bijwerken, verwijderen). Hier is een basisinterface voor de gegevenslaag:
package abstract_data_layer import "time" type Song struct Url string Naam string Beschrijving string type Label struct Naam string type Gebruiker struct Naam string Email string RegisteredAt time.Time LastLogin time.Time type DataLayer interface // Queries (lees -only) GetUsers () ([] Gebruiker, fout) GetUserByEmail (e-mailstring) (Gebruiker, fout) GetLabels () ([] Label, fout) GetSongs () ([] Nummer, fout) GetSongsByUser (user User) ([ ] Song, fout) GetSongsByLabel (labelstring) ([] Song, fout) // Veranderingen in status CreateUser (user User) error ChangeUserName (user User, name string) error AddLabel (label string) error AddSong (user User, song Song , labels [] Label) fout
Merk op dat het doel van dit domeinmodel is om een eenvoudige maar niet volledig triviale gegevenslaag te presenteren om de testaspecten te demonstreren. Vanzelfsprekend zullen er in een echte applicatie meer objecten zijn zoals albums, genres, artiesten en nog veel meer informatie over elk nummer. Als het erop aan komt, kun je altijd willekeurige informatie over een nummer opslaan in de beschrijving, en zoveel labels toevoegen als je wilt.
In de praktijk wilt u uw gegevenslaag mogelijk opsplitsen in meerdere interfaces. Sommige van de structs hebben mogelijk meer kenmerken en de methoden kunnen meer argumenten vereisen (bijvoorbeeld alle GetXXX ()
methoden zullen waarschijnlijk enkele pagingargumenten vereisen). Mogelijk hebt u andere interfaces voor gegevenstoegang en methoden voor onderhoudswerkzaamheden nodig, zoals laden in bulk, back-ups en migraties. Soms is het zinvol om in plaats daarvan of in aanvulling op de synchrone interface een asynchrone gegevenstoeganginterface te ontmaskeren.
Wat hebben we geleerd van deze abstracte gegevenslaag?
De gegevens kunnen worden opgeslagen in meerdere gedistribueerde datastores, op meerdere clusters over verschillende geografische locaties in een combinatie van lokale datacenters en de cloud.
Er zullen storingen optreden en die storingen moeten worden aangepakt. Idealiter kan de foutafhandelingslogica (pogingen, time-outs, melding van catastrofale storingen) worden afgehandeld door de betonnen gegevenslaag. De logica-logica-code moet alleen de gegevens terugkrijgen of een generieke fout als de gegevens niet bereikbaar zijn.
In sommige gevallen kan de domeinlogica meer granulaire toegang tot de gegevens verlangen en in bepaalde situaties een fallback-strategie selecteren (bijvoorbeeld zijn slechts gedeeltelijke gegevens beschikbaar omdat een deel van het cluster ontoegankelijk is of de gegevens oud zijn omdat de cache niet is vernieuwd ). Deze aspecten hebben implicaties voor het ontwerp van uw gegevenslaag en voor het testen ervan.
Voor zover het testen gaat, zou u uw eigen fouten moeten teruggeven die in de abstracte gegevenslaag worden bepaald en alle concrete foutenmeldingen aan uw eigen foutentypes toewijzen of op zeer generieke foutenberichten vertrouwen.
Laten we onze gegevenslaag bespotten. Het doel van de mock is om de echte gegevenslaag tijdens tests te vervangen. Dat vereist dat de nepgegevenslaag dezelfde interface blootstelt en op elke reeks methoden kan reageren met een ingeblikt (of berekend) antwoord.
Bovendien is het handig om bij te houden hoe vaak elke methode is aangeroepen. Ik zal het hier niet demonstreren, maar het is zelfs mogelijk om de volgorde van oproepen naar verschillende methoden bij te houden en welke argumenten zijn doorgegeven aan elke methode om een bepaalde reeks oproepen te garanderen.
Hier is de nep datalaag struct.
package concrete_data_layer import (. "abstract_data_layer") const (GET_USERS = iota GET_USER_BY_EMAIL GET_LABELS GET_SONGS GET_SONGS_BY_USER GET_SONG_BY_LABEL FOUTEN) type MockDataLayer struct Fouten [] fout GetUsersResponses [] [] Gebruiker GetUserByEmailResponses [] Gebruiker GetLabelsResponses [] [] Label GetSongsResponses [] [] Song GetSongsByUserResponses [] [] Song GetSongsByLabelResponses [] [] Songindexen [] int func NewMockDataLayer () MockDataLayer return MockDataLayer Indices: [] int 0, 0, 0, 0, 0, 0, 0
De const
statement geeft een overzicht van alle ondersteunde bewerkingen en fouten. Elke bewerking heeft zijn eigen index in de index
plak. De index voor elke bewerking geeft aan hoe vaak de overeenkomstige methode is aangeroepen en wat de volgende reactie en fout zou moeten zijn.
Voor elke methode met een geretourneerde waarde naast een fout, is er een reeks antwoorden. Wanneer de voorbeeldmethode wordt aangeroepen, worden het bijbehorende antwoord en de bijbehorende fout (op basis van de index voor deze methode) geretourneerd. Voor methoden die geen geretourneerde waarde hebben behalve een fout, hoeft u geen a te definiëren XXXResponses
plak.
Merk op dat de fouten door alle methoden worden gedeeld. Dat betekent dat als u een reeks oproepen wilt testen, u het juiste aantal fouten in de juiste volgorde moet injecteren. Een alternatief ontwerp zou voor elk antwoord een paar gebruiken dat bestaat uit de geretourneerde waarde en fout. De NewMockDataLayer ()
functie retourneert een nieuwe mock-gegevenslaag-struct met alle indexen die op nul zijn geïnitialiseerd.
Hier is de implementatie van de GetUsers ()
methode, die deze concepten illustreert.
func (m * MockDataLayer) GetUsers () (gebruikers [] Gebruiker, fout err) i: = m.Indices [GET_USERS] gebruikers = m.GetUsersResponses [i] if len (m.Errors)> 0 err = m. Fouten [m.Indices [ERRORS]] m.Indices [ERRORS] ++ m.Indices [GET_USERS] ++ return
De eerste regel krijgt de huidige index van de GET_USERS
bewerking (wordt aanvankelijk 0).
De tweede regel krijgt het antwoord voor de huidige index.
De derde tot en met vijfde regel wijzen de fout van de huidige index toe als de fouten
veld werd ingevuld en de foutenindex verhoogd. Bij het testen van het gelukkige pad, zal de fout nul zijn. Om het gemakkelijker te gebruiken te maken, kunt u het initialiseren van het fouten
veld en dan zal elke methode voor de fout nul teruggeven.
De volgende regel verhoogt de index, zodat de volgende oproep de juiste reactie krijgt.
De laatste regel keert gewoon terug. De genoemde retourwaarden voor gebruikers en err zijn al ingevuld (of standaard nul voor err).
Hier is een andere methode, GetLabels ()
, die hetzelfde patroon volgt. Het enige verschil is welke index wordt gebruikt en welke verzameling standaardantwoorden wordt gebruikt.
func (m * MockDataLayer) GetLabels () (labels [] Label, fout err) i: = m.Indices [GET_LABELS] labels = m.GetLabelsResponses [i] als len (m.Errors)> 0 err = m. Fouten [m.Indices [ERRORS]] m.Indices [ERRORS] ++ m.Indices [GET_LABELS] ++ return
Dit is een goed voorbeeld van een use-case waarbij generieke geneesmiddelen een a kunnen opslaan lot van boilerplate code. Het is mogelijk om te profiteren van reflectie met hetzelfde effect, maar het valt buiten het bestek van deze zelfstudie. De belangrijkste take-away hier is dat de nepgegevenslaag een patroon voor algemene doeleinden kan volgen en elk testscenario kan ondersteunen, zoals u snel zult zien.
Hoe zit het met sommige methoden die alleen maar een fout retourneren? Bekijk de CreateUser ()
methode. Het is zelfs eenvoudiger omdat het alleen fouten behandelt en de standaardantwoorden niet hoeft te beheren.
func (m * MockDataLayer) CreateUser (user User) (err error) if len (m.Errors)> 0 i: = m.Indices [CREATE_USER] err = m.Errors [m.Indices [ERRORS]] m. Indices [FOUTEN] ++ retour
Deze nepgegevenslaag is slechts een voorbeeld van wat er nodig is om een interface te bespotten en een aantal nuttige services aan te bieden om te testen. Je kunt een eigen mock-implementatie bedenken of beschikbare mock-bibliotheken gebruiken. Er is zelfs een standaard GoMock-raamwerk.
Persoonlijk vind ik mock-frameworks eenvoudig te implementeren en geef ik er de voorkeur aan mijn eigen frameworks te rollen (vaak automatisch genereren) omdat ik het grootste deel van mijn ontwikkeltijd schrijftesten en spotafhankelijkheden doorbreng. YMMV.
Nu we een mock-gegevenslaag hebben laten we er enkele tests tegen schrijven. Het is belangrijk om te weten dat we hier de gegevenslaag zelf niet testen. We zullen de gegevenslaag zelf testen met andere methoden verderop in deze serie. Het doel is hier om de logica van de code te testen die afhankelijk is van de abstracte gegevenslaag.
Stel dat een gebruiker een nummer wil toevoegen, maar we hebben een limiet van 100 nummers per gebruiker. Het verwachte gedrag is dat als de gebruiker minder dan 100 nummers heeft en het toegevoegde nummer nieuw is, het wordt toegevoegd. Als het nummer al bestaat, wordt de foutmelding 'Dubbele song' geretourneerd. Als de gebruiker al 100 liedjes heeft, retourneert het de foutmelding "Overdikte songquotum".
Laten we een test schrijven voor deze testgevallen met onze nagebootste gegevenslaag. Dit is een white-box-test, wat betekent dat u moet weten welke methoden van de datalaag de te testen code moet gebruiken en in welke volgorde, zodat u de schijnaanvragen en fouten op de juiste manier kunt invullen. Dus de test-first benadering is hier niet ideaal. Laten we eerst de code schrijven.
Hier is de SongManager
struct. Het hangt alleen af van de abstracte gegevenslaag. Hiermee kunt u een implementatie van een echte gegevenslaag in productie doorgeven, maar een nagebootste gegevenslaag tijdens het testen.
De SongManager
zelf is volledig agnostisch voor de concrete uitvoering van de dataLayer
interface. De SongManager
struct accepteert ook een gebruiker, die het opslaat. Vermoedelijk heeft elke actieve gebruiker zijn eigen gebruiker SongManager
bijvoorbeeld, en gebruikers kunnen alleen nummers voor zichzelf toevoegen. De NewSongManager ()
functie zorgt voor de invoer dataLayer
interface is niet nul.
package song_manager import ("errors". "abstract_data_layer") const (MAX_SONGS_PER_USER = 100) type SongManager struct user Gebruiker dal DataLayer func NewSongManager (user User, dal DataLayer) (* SongManager, error) if dal == nil ga terug nul, errors.New ("DataLayer kan niet nul zijn") return & SongManager user, dal, nil
Laten we een implementeren AddSong ()
methode. De methode roept de datalaag's aan GetSongsByUser ()
eerst, en vervolgens doorloopt een aantal controles. Als alles in orde is, roept het de gegevenslagen op AddSong ()
methode en retourneert het resultaat.
func (lm * SongManager) AddSong (newSong Song, labels [] Label) error songs, err: = lm.dal.GetSongsByUser (lm.user) if err! = nil return nil // Controleer of nummer een duplicaat is for _, song: = range songs if song.Url == newSong.Url return errors.New ("Duplicate song") // Controleer of gebruiker maximumaantal nummers heeft als len (liedjes) == MAX_SONGS_PER_USER return errors.New ("Song-quota overschreden") return lm.dal.AddSong (user, newSong, labels)
Als u naar deze code kijkt, ziet u dat er twee andere testcases zijn die we hebben verwaarloosd: de aanroepen van de methoden van de gegevenslaag GetSongByUser ()
en AddSong ()
kan om andere redenen mislukken. Nu, met de implementatie van SongManager.AddSong ()
voor ons kunnen we een uitgebreide test schrijven die alle use-cases dekt. Laten we beginnen met het gelukkige pad. De TestAddSong_Success ()
methode maakt een gebruiker genaamd Gigi en een onechte gegevenslaag.
Het vult de GetSongsByUserResponses
veld met een segment dat een leeg segment bevat, wat resulteert in een leeg segment wanneer de SongManager belt GetSongsByUser ()
op de mock-gegevenslaag zonder fouten. Het is niet nodig om iets te doen voor de oproep naar de mock-gegevenslagen AddSong ()
methode, die standaard een fout nul zal retourneren. De test verifieert alleen dat er inderdaad geen fout is geretourneerd door de bovenliggende aanroep naar de SongManager's AddSong ()
methode.
package song_manager import ("testen". "abstract_data_layer". "concrete_data_layer") func TestAddSong_Success (t * testing.T) u: = Gebruiker Name: "Gigi", Email: "[email protected]" mock: = NewMockDataLayer () // Mock responses voorbereiden mock.GetSongsByUserResponses = [] [] Song lm, err: = NewSongManager (u, & mock) if err! = Nil t.Error ("NewSongManager () is 'nihil' teruggegeven ") url: = https://www.youtube.com/watch?v=MlW7T0SUH0E" err = lm.AddSong (Song URL: url ", Name:" Chacarron ", nil) if fout! = nil t.Error ("AddSong () failed") $ go test PASS ok song_manager 0.006s
Het testen van foutcondities is ook super eenvoudig. U hebt volledige controle over wat de gegevenslaag van de oproepen naar retourneert GetSongsByUser ()
en AddSong ()
. Hier is een test om te controleren of je bij het toevoegen van een duplicaatnummer de juiste foutmelding terug krijgt.
func TestAddSong_Duplicate (t * testing.T) u: = Gebruiker Name: "Gigi", Email: "[email protected]" mock: = NewMockDataLayer () // Maak nepantwoorden klaar mock.GetSongsByUserResponses = [] [] Lied testSong lm, err: = NewSongManager (u, & mock) if err! = Nil t.Error ("NewSongManager ()" nil '") err = lm.AddSong (testSong, nil) if err == nil t.Error ("AddSong () should should failed") if err.Error ()! = "Duplicate song" t.Error ("AddSong () wrong error:" + err.Error ())
De volgende twee testcases testen of het juiste foutbericht wordt geretourneerd wanneer de gegevenslaag zelf niet werkt. In het eerste geval de datalaag GetSongsByUser ()
geeft een foutmelding.
func TestAddSong_DataLayerFailure_1 (t * testing.T) u: = Gebruiker Name: "Gigi", Email: "[email protected]" mock: = NewMockDataLayer () // Bereid schijnresponses voor mock.GetSongsByUserResponses = [] [] Lied e: = errors.New ("getSongsByUser () failure") mock.Errors = [] error e lm, err: = NewSongManager (u, & mock) if err! = Nil t.Error ( "NewSongManager () terug 'nul'") err = lm.AddSong (testSong, nihil) if err == nil t.Error ("AddSong () should should failed") if err.Error ()! = " GetSongsByUser () failure "t.Error (" AddSong () verkeerde fout: "+ err.Error ())
In het tweede geval zijn de gegevenslagen AddSong ()
methode retourneert een fout. Sinds de eerste oproep naar GetSongsByUser ()
zou moeten slagen, de mock.Errors
plak bevat twee items: nul voor de eerste oproep en de fout voor de tweede oproep.
func TestAddSong_DataLayerFailure_2 (t * testing.T) u: = Gebruiker Name: "Gigi", Email: "[email protected]" mock: = NewMockDataLayer () // Bereid schijnresponses voor mock.GetSongsByUserResponses = [] [] Nummer e: = errors.New ("AddSong () failure") mock.Errors = [] error nil, e lm, err: = NewSongManager (u, & mock) if err! = Nil t. Fout ("NewSongManager () retourneerde 'nul'") err = lm.AddSong (testSong, nihil) if err == nil t.Error ("AddSong () should should failed") if err.Error ()! = "AddSong () failure" t.Error ("AddSong () verkeerde fout:" + err.Error ())
In deze zelfstudie hebben we het concept van een abstracte gegevenslaag geïntroduceerd. Vervolgens hebben we met behulp van het persoonlijke muziekbeheerdomein aangetoond hoe een gegevenslaag moet worden ontworpen, een schijngegevenslaag kan worden gemaakt en de schijngegevenslaag kan worden gebruikt om de toepassing te testen.
In deel twee zullen we ons concentreren op testen met behulp van een echte in-memory gegevenslaag. Blijf kijken.