Multe sisteme non-trivial sunt, de asemenea, date-intensive sau bazate pe date. Testarea părților sistemelor cu intensitate mare de date este foarte diferită de cea a sistemelor cu intensitate de codificare. În primul rând, poate exista o mulțime de sofisticare în stratul de date în sine, cum ar fi stocurile hibride de date, cache, backup și redundanță.
Toate aceste mașini nu au nimic de-a face cu aplicația în sine, ci trebuie testate. În al doilea rând, codul poate fi foarte generic și, pentru al testa, trebuie să generați date care sunt structurate într-un anumit mod. În această serie de cinci tutoriale, mă voi referi la toate aceste aspecte, vom explora mai multe strategii pentru proiectarea sistemelor testabile cu date intensive cu Go, și vom arunca cu capul în exemple concrete.
În prima parte, voi trece proiectul unui strat de date abstract care permite testarea corectă, modul de a face manipularea erorilor în stratul de date, cum să falsizați codul de acces la date și cum să testați un strat de date abstract.
Confruntarea cu magazinele de date reale și complicațiile lor este complicată și nu are legătură cu logica afacerii. Conceptul de strat de date vă permite să expuneți o interfață elegantă la datele dvs. și să ascundeți detaliile despre exact cum sunt stocate datele și cum să le accesați. Voi folosi o aplicație de probă numită "Songify" pentru gestionarea muzicii personale pentru a ilustra conceptele cu cod real.
Să examinăm domeniul personal de gestionare a muzicii - utilizatorii pot adăuga melodii și le pot eticheta - și să analizeze ce date trebuie să stocăm și cum să le accesăm. Obiectele din domeniul nostru sunt utilizatori, melodii și etichete. Există două categorii de operații pe care doriți să le efectuați pe orice date: interogări (numai pentru citire) și modificări de stare (creați, actualizați, ștergeți). Iată o interfață de bază pentru stratul de date:
pachet abstract_data_layer tipul de timp de import "structură de timp" Song struct șir Url șir de nume șir de caractere tip Etichetă struct Nume șir tip Structură utilizator Numele șir E-mail șir RegisteredAt time.Time LastLogin time.Time tip Interfață DataLayer // Queries -Ental) GetUsers () ([] Utilizator, eroare) GetUserByEmail (utilizator, eroare) GetLabels () ] Song, eroare) GetSongsByLabel (șir de etichete) ([] Song, eroare) // Eroare ChangeUserName (utilizator utilizator, șir de nume) eroare AddLabel (eroare de etichetă) , etichete [] Etichetă) eroare
Rețineți că scopul acestui model de domeniu este să prezinte un strat de date simplu, dar nu complet trivial, pentru a demonstra aspectele de testare. Evident, într-o aplicație reală vor fi mai multe obiecte, cum ar fi albume, genuri, artiști și multe informații despre fiecare melodie. În cazul în care apăsați împingeți, puteți întotdeauna să stocați informații arbitrare despre un cântec în descrierea sa, precum și să atașați cât mai multe etichete pe care le doriți.
În practică, poate doriți să împărțiți stratul de date în mai multe interfețe. Unele structuri pot avea mai multe atribute, iar metodele pot necesita mai multe argumente (de exemplu, toate GetXXX ()
metodele probabil vor necesita unele argumente de paginare). Este posibil să aveți nevoie de alte interfețe de acces la date și de metode pentru operațiile de întreținere, cum ar fi încărcarea în vrac, copii de rezervă și migrațiile. Este uneori logic să se expună în schimb o interfață de acces la date asincronă sau în plus față de interfața sincronă.
Ce am câștigat din acest strat de date abstract?
Datele pot fi stocate în mai multe depozite de date distribuite, pe mai multe clustere în diferite locații geografice într-o combinație de centre de date on-premise și cloud.
Vor exista eșecuri și aceste eșecuri trebuie tratate. În mod ideal, logica de gestionare a erorilor (încercări, expirări, notificări de defecțiuni catastrofale) poate fi tratată de stratul de date concret. Codul logic al domeniului ar trebui să primească doar date înapoi sau o eroare generică atunci când datele sunt inaccesibile.
În unele cazuri, logica domeniului poate să aibă nevoie de un acces mai granular la date și să selecteze o strategie de rezervă în anumite situații (de exemplu, sunt disponibile numai date parțiale deoarece o parte a clusterului este inaccesibilă sau datele sunt învechite deoarece memoria cache nu a fost actualizată ). Aceste aspecte au implicații pentru proiectarea stratului dvs. de date și pentru testarea acestuia.
În ceea ce privește testarea, ar trebui să returnați propriile erori definite în stratul de date abstract și să mapați toate mesajele de eroare concrete către propriile tipuri de erori sau să vă bazați pe mesaje de eroare generice foarte generale.
Să frământăm stratul nostru de date. Scopul machetei este înlocuirea stratului real de date în timpul testelor. Aceasta presupune ca stratul de date machete să expună aceeași interfață și să poată răspunde la fiecare secvență de metode cu un răspuns conservat (sau calculat).
În plus, este util să urmăriți de câte ori a fost apelată fiecare metodă. Nu o voi demonstra aici, dar este chiar posibil să urmăriți ordinea apelurilor la diferite metode și care sunt argumentele transmise fiecărei metode pentru a asigura un anumit lanț de apeluri.
Iată structura stratului de date machete.
import pachet concrete_data_layer (. "abstract_data_layer") const (GET_USERS = iota GET_USER_BY_EMAIL GET_LABELS GET_SONGS GET_SONGS_BY_USER GET_SONG_BY_LABEL ERORILE) tip MockDataLayer struct Erori [] GetUsersResponses eroare [] [] GetUserByEmailResponses utilizator [] GetLabelsResponses Utilizator [] [] GetSongsResponses Label [] [] Song [] [] [] [] int [0, 0, 0, 0, 0, 0, 0, 0
const
declarația afișează toate operațiile acceptate și erorile. Fiecare operațiune are un index propriu în indici
felie. Indicele pentru fiecare operație reprezintă de câte ori a fost apelată metoda corespunzătoare, precum și ce răspuns și eroare ar trebui să fie următorul.
Pentru fiecare metodă care are o valoare de returnare în plus față de o eroare, există o felie de răspunsuri. Atunci când se numește metoda mock, răspunsul și eroarea corespunzătoare (pe baza indexului pentru această metodă) sunt returnate. Pentru metodele care nu au o valoare de returnare, cu excepția unei erori, nu este nevoie să definiți a XXXResponses
felie.
Rețineți că erorile sunt împărtășite prin toate metodele. Aceasta înseamnă că, dacă doriți să testați o serie de apeluri, va trebui să introduceți numărul corect de erori în ordinea corectă. Un design alternativ ar folosi pentru fiecare răspuns o pereche constând din valoarea retur și eroarea. NewMockDataLayer ()
funcția returnează un nou structură structură de date machetă cu toți indici inițializați la zero.
Iată punerea în aplicare a GetUsers ()
care ilustrează aceste concepte.
Funcționează (m * MockDataLayer) GetUsers () (utilizatori [] Utilizator, eroare eroare) i: = m.Indice [GET_USERS] users = m.GetUsersResponses [i] if len (m.Errors)> 0 err = m. Erori [m.Indici [ERRORS]] m.Indice [ERORI] ++ m.Indice [GET_USERS] ++ returnare
Prima linie primește indicele curent al GET_USERS
(va fi inițial 0).
A doua linie primește răspunsul pentru indexul curent.
Cea de-a treia până la a cincea linie atribuie eroarea indicelui curent în cazul în care Erori
câmpul a fost populat și crește indicele de erori. Când se testează calea fericită, eroarea va fi zero. Pentru a ușura utilizarea, puteți evita inițializarea Erori
câmp și apoi fiecare metodă va reveni la zero pentru eroare.
Următorul rând crește indicele, astfel încât următorul apel va primi răspunsul adecvat.
Ultima linie se întoarce. Valorile returnate numite pentru utilizatori și err sunt deja populate (sau zero în mod implicit pentru eroare).
Iată o altă metodă, GetLabels ()
, care urmează aceluiași model. Singura diferență este ce index este folosit și ce colecție de răspunsuri conservate este utilizată.
functie m (m * MockDataLayer) GetLabels () (etichete [] Eticheta, eroare eroare) i: = m.Indice [GET_LABELS] etichete = m.GetLabelsResponses [i] if len (m.Errors)> 0 err = m. Erori [m.Indici [ERRORS]] m.Indice [ERORI] ++ m.Indice [GET_LABELS] ++ returnare
Acesta este un prim exemplu al unui caz de utilizare în care medicamentele generice ar putea salva a mult codului de boilerplate. Este posibil să profiți de reflecție în același sens, dar este în afara scopului acestui tutorial. Scopul principal al acestui demers este acela că stratul de date fals poate urma un model general și sprijină orice scenariu de testare, așa cum veți vedea în curând.
Ce zici de unele metode care returnează o eroare? Verificați Creaza utilizator()
metodă. Este chiar mai simplu, deoarece se ocupă doar de erori și nu are nevoie să gestioneze răspunsurile conservate.
func (m * MockDataLayer) CreateUser (utilizator utilizator) (eroare eroare) if len (m.Errors)> 0 i: = m.Indices [CREATE_USER] err = m.Errors [m.Indices [ERRORS] Indici [ERRORS] ++ return
Acest tip de date machete este doar un exemplu de ceea ce este nevoie pentru a bate o interfață și pentru a oferi câteva servicii utile pentru a testa. Aveți posibilitatea să veniți cu propria implementare de machete sau să folosiți bibliotecile disponibile. Există chiar și un cadru GoMock standard.
Eu personal găsesc cadru simplificat ușor de implementat și prefer să-mi scot propriile (adesea generându-le automat) pentru că îmi petrec cea mai mare parte a timpului meu de dezvoltare, scriind teste și dependențe de batjocură. YMMV.
Acum, că avem un strat de date fals, să scriem câteva teste împotriva acestuia. Este important să ne dăm seama că aici nu testăm stratul de date în sine. Vom testa stratul de date în sine cu alte metode mai târziu din această serie. Scopul aici este de a testa logica codului care depinde de stratul de date abstract.
De exemplu, să presupunem că un utilizator dorește să adauge o melodie, dar avem o cotă de 100 de melodii pe utilizator. Comportamentul așteptat este că, dacă utilizatorul are mai puțin de 100 de melodii și melodia adăugată este nouă, va fi adăugată. Dacă piesa există deja, atunci ea returnează o eroare "Cântec duplicat". Dacă utilizatorul are deja 100 de melodii, atunci acesta returnează o eroare "Cresterea cântecului".
Să scriem un test pentru aceste cazuri de testare folosind stratul nostru de date machete. Acesta este un test de tip "white-box", ceea ce înseamnă că trebuie să știți care metode ale stratului de date pe care codul test le va apela și în ce ordine puteți să răspândiți corect răspunsurile și erorile. Deci, abordarea test-prima nu este ideală aici. Să scriem mai întâi codul.
Aici este SongManager
struct. Depinde doar de stratul abstract de date. Acest lucru vă va permite să transmiteți o implementare a unui strat de date real în producție, dar un strat de date machete în timpul testării.
SongManager
ea însăși este complet agnostică pentru punerea în aplicare concretă a dataLayer
interfață. SongManager
struct acceptă, de asemenea, un utilizator, pe care îl stochează. Probabil, fiecare utilizator activ are propriile sale SongManager
exemplu, iar utilizatorii pot adăuga numai melodii pentru ei înșiși. NewSongManager ()
funcția asigură intrarea dataLayer
interfața nu este zero.
pachetul song_manager import ("erori", "abstract_data_layer") const (MAX_SONGS_PER_USER = 100) tip SongManager struct utilizator User dal DataLayer func NewSongManager (user User, dal DataLayer) if = null, errors.New ("DataLayer nu poate fi nul") return & SongManager user, dal, nil
Să punem în aplicare o AddSong ()
metodă. Metoda numește stratul de date GetSongsByUser ()
în primul rând, și apoi trece prin mai multe verificări. Dacă totul este OK, acesta apelează stratul de date AddSong ()
și returnează rezultatul.
(lm.user) err! = nil return nil // Verificați dacă melodia este un duplicat pentru _, song: = melodii în intervalul if song.Url == newSong.Url return errors.New ("Duplicate song") // Verificați dacă utilizatorul are un număr maxim de melodii dacă len (melodii) == MAX_SONGS_PER_USER returnează erorile.New ("cota Song a fost depășită") returnați lm.dal.AddSong (utilizator, newSong, etichete)
Privind la acest cod, puteți vedea că există și alte două cazuri de testare pe care le-am neglijat: apelurile către metodele stratului de date GetSongByUser ()
și AddSong ()
ar putea eșua din alte motive. Acum, cu punerea în aplicare a SongManager.AddSong ()
în fața noastră, putem scrie un test cuprinzător care acoperă toate cazurile de utilizare. Să începem cu calea fericită. TestAddSong_Success ()
metoda creează un utilizator numit Gigi și un strat de date machete.
Este populat de GetSongsByUserResponses
câmp cu o felie care conține o felie goală, care va avea ca rezultat o felie goală atunci când apelul SongManager va fi trimis GetSongsByUser ()
pe stratul de date machete fără eroare. Nu este nevoie să faceți nimic pentru apelul la stratul de date machete AddSong ()
, care va returna în mod implicit eroarea zero. Testul verifică doar că nu a fost returnată nicio eroare de la apelul părinte la SongManager AddSong ()
metodă.
pachetul song_manager import ("testarea", "abstract_data_layer", "concrete_data_layer") func TestAddSong_Success (t * testing.T) u: = Utilizator Nume: "Gigi", Email: "[email protected]" mock: = NewMockDataLayer () // Pregătiți răspunsuri mock.GetSongsByUserResponses = [] [] Song lm, err: = NewSongManager (u & mock) dacă err! = Nil t.Error " url: = https://www.youtube.com/watch?v=MlW7T0SUH0E" err = lm.AddSong (Song Url: url ", Nume:" Chacarron " t.Error ("AddSong () nu a reușit") $ go test PASS ok song_manager 0.006s
Testarea condițiilor de eroare este foarte ușoară. Aveți control complet asupra a ceea ce returnează stratul de date de la apelurile către GetSongsByUser ()
și AddSong ()
. Iată un test pentru a verifica faptul că atunci când adaugi un cântec duplicat primești mesajul de eroare corespunzător înapoi.
func TestAddSong_Duplicate (t * testing.T) u: = Utilizator Nume: "Gigi", Email: "[email protected]" mock: = NewMockDataLayer Err = lm.AddSong (testSong, nil) dacă err! = Nil t.Error ("NewSongManager () == nil t.Error ("AddSong () ar fi trebuit să fi eșuat") dacă err.Error ()! = "Cântec duplicat" t.Error
Următoarele două cazuri de test testează faptul că mesajul de eroare corect este returnat atunci când stratul de date nu reușește. În primul caz, stratul de date este GetSongsByUser ()
returnează o eroare.
func TestAddSong_DataLayerFailure_1 (t * test.T) u: = Utilizator Nume: "Gigi", Email: "[email protected]" mock: = NewMockDataLayer Song e: = errors.New ("Eșecul GetSongsByUser ()" mock.Errors = [] eroare e lm, err: = NewSongManager "NewSongManager () a returnat 'nil'") err = lm.AddSong (testSong, nil) dacă err == nil t.Error (" GetSongsByUser () eșec "t.Error (" AddSong () eroare greșită: "+ err.Error ())
În al doilea caz, stratul de date este AddSong ()
metoda returnează o eroare. De la primul apel la GetSongsByUser ()
ar trebui să reușească, mock.Errors
slice conține două elemente: zero pentru primul apel și eroarea pentru al doilea apel.
func TestAddSong_DataLayerFailure_2 (t * testing.T) u: = Utilizator Nume: "Gigi", Email: "[email protected]" mock: = NewMockDataLayer Song e: = errors.New ("Eșecul AddSong") mock.Errors = [] eroare nil, e lm, err: = NewSongManager (u & mock) Eroare ("NewSongManager () a returnat" nil "") err = lm.AddSong (testSong, nil) dacă err == nil t.Error () = "Eroare AddSong ()" t.Error ("AddSong () eroare greșită:" + err.Error ())
În acest tutorial am introdus conceptul de strat abstract de date. Apoi, utilizând domeniul personal de gestionare a muzicii, am demonstrat cum să proiectăm un strat de date, să construim un strat de date machete și să folosim stratul de date machete pentru a testa aplicația.
În partea a doua, ne vom concentra pe testarea folosind un strat real de date în memorie. Rămâneți aproape.