Testarea codului intensiv de date cu ajutorul porții, partea 2

Prezentare generală

Aceasta este o parte din două din cinci într-o serie de tutori privind testarea codului intensiv de date. În prima parte, am acoperit designul unui strat de date abstract care permite testarea corectă, modul de gestionare a erorilor în stratul de date, cum să falsificați codul de acces la date și cum să testați un strat de date abstract. În acest tutorial, voi trece peste testarea unui strat real de date în memorie bazat pe popularul SQLite. 

Testarea împotriva unui depozit de date în memorie

Testarea împotriva unui strat de date abstract este excelentă pentru unele cazuri de utilizare în care aveți nevoie de o mare precizie, înțelegeți exact ce apeluri codul de test se va face împotriva stratului de date și sunteți pregătit de răspunsurile mândru.

Uneori, nu este așa de ușor. Seria de apeluri către nivelul de date poate fi dificil de constatat sau este nevoie de mult efort pentru a pregăti răspunsurile corecte adecvate, care sunt valabile. În aceste cazuri, poate fi necesar să lucrați împotriva unui depozit de date în memorie. 

Beneficiile unui magazin de date în memorie sunt:

  • Este foarte rapid. 
  • Lucrezi împotriva unui magazin de date real.
  • Puteți să o populați adesea de la zero utilizând fișiere sau coduri.

În special, dacă magazinul de date este un DB relațional, SQLite este o opțiune fantastică. Nu uitați că există diferențe între SQLite și alte DB-uri relaționale populare, cum ar fi MySQL și PostgreSQL.

Asigurați-vă că vă răspundeți la aceasta în testele dvs. Rețineți că accesați în continuare datele dvs. prin stratul de date abstract, însă acum magazinul de asistență în timpul testelor este stocarea datelor din memorie. Testul dvs. va umple datele de testare în mod diferit, dar codul testat nu cunoaște bine ceea ce se întâmplă.

Folosind SQLite

SQLite este un DB încorporat (legat de aplicația dvs.). Nu există un server DB separat care rulează. De obicei, stochează datele într-un fișier, dar are, de asemenea, opțiunea unui magazin de susținere în memorie. 

Aici este InMemoryDataStore struct. Este, de asemenea, parte din concrete_data_layer pachet și importă pachetul third-party go-sqlite3 care implementează interfața standard Golang "database / sql".  

pachet concrete_data_layer structură db * sql.DB import ("database / sql"; "abstract_data_layer" _ "github.com/mattn/go-sqlite3" "time" "fmt"

Construirea stratului de date în memorie

NewInMemoryDataLayer () funcția constructor creează un DB sqlite în memorie și returnează un pointer la InMemoryDataLayer

Funcționează în mod repetat, în cazul în care err! = nil return nil, err err = createSqliteSchema (db) return & InMemoryDataLayer  db, nil 

Rețineți că de fiecare dată când deschideți un nou ": memory:" DB, începeți de la zero. Dacă doriți persistență în cadrul mai multor apeluri către NewInMemoryDataLayer (), ar trebui să utilizați fișier :: memorie:? cache = partajat. Vedeți acest thread de discuție GitHub pentru mai multe detalii.

InMemoryDataLayer implementează dataLayer interfață și de fapt stochează datele cu relații corecte în baza de date sqlite. Pentru a face acest lucru, trebuie mai întâi să creați o schemă adecvată, care este exact sarcina createSqliteSchema () în constructor. Creează trei tabele de date - cântece, utilizatori și etichete - și două tabele de referință încrucișate, label_song și user_song.

Se adaugă unele constrângeri, indici și chei străine pentru a se compara tabelele între ele. Nu mă voi concentra asupra detaliilor specifice. Principalul lucru este că întreaga schemă DDL este declarată ca un singur șir (constând din mai multe instrucțiuni DDL) care sunt apoi executate utilizând db.Exec () metodă, iar dacă ceva nu merge bine, se întoarce o eroare. 

functie creareSqliteSchema (db * sql.DB) eroare schema: = 'CREATE TABLE DACA NU EXISTS song (id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT UNIQUE, nume TEXT, descriere TEXT); CREATE TABEL DACĂ NU ESTE EXISTĂ utilizator (id INTEGER PRIMARY KEY AUTOINCREMENT, nume TEXT, email TEXT UNIQUE, registered_at TIMESTAMP, last_login TIMESTAMP); CREAȚI INDEX user_email_idx ON user (e-mail); CREAȚI TABELUL DACĂ NU ESTE Eticheta EXISTS (id INTEGER PRIMARY KEY AUTOINCREMENT, nume TEXT UNIQUE); CREAȚI INDEX label_name_idx ON label (nume); CREATE TABLE DACĂ NU EXISTĂ eticheta label_song (label_id INTEGER NOT NULL REFERENCES etichetă (id), song_id INTEGER NOT NULL REFERINȚE song (id), KEY PRIMARY (label_id, song_id)); CREATE TABLE DACĂ NU EXISTĂ user_song (user_id INTEGER NOT NULL REFERINȚE user (id), song_id INTEGER NOT NULL REFERINȚE song (id), KEY PRIMARY (user_id, song_id) _, err: = db.Exec (schema) retur eroare 

Este important să ne dăm seama că, în timp ce SQL este standard, fiecare sistem de gestionare a bazelor de date (DBMS) are propriul său gust și definiția exactă a schemei nu va funcționa neapărat ca și pentru un alt DB.

Implementarea stratului de date în memorie

Pentru a vă oferi un gust al efortului de implementare al unui strat de date în memorie, iată câteva metode: AddSong () și GetSongsByUser ()

AddSong () metoda face o mulțime de muncă. Se inserează o înregistrare în cântec tabel, precum și în fiecare dintre tabelele de referință: label_song și user_song. În fiecare punct, în cazul în care o operație eșuează, aceasta returnează o eroare. Nu folosesc niciun fel de tranzacții deoarece este proiectat doar pentru scopuri de testare și nu mă îngrijorează despre datele parțiale din DB.

funcția (), folosiți melodia AddSong (utilizatorul utilizatorului, melodia Song, etichetele [] Etichetă) eroare s: = .db.Prepare dacă err! = nil return err rezultat, err: = statement.Exec (song.Url, song.Name, song.Description) dacă err! = nil return err songId, err: = result.LastInsertId () dacă err! = nil return err s = "SELECT id FROM utilizator unde e-mail =?" rr, err: = m.db.Query (s, user.Email) dacă err! = nil retur err var userId int pentru rânduri.Next () err = retur err s = 'instrucțiuni INSERT IN user_song (user_id, song_id) (?,?)', err = m.db.Prepare dacă err! = nil return err _, err = statement.Exec (userId, songId) dacă err! = nil retur err var etichetăId int64 s: = "INSERT INTO label (nume) valori (?)" label_ins, err: = m.db.Prepare dacă err! return err s = 'INSERT IN label_song (?,?)' label_song_ins, err: = m.db.Prepare dacă err! = nil return err pentru _, t: = etichete cu etichete s = "SELECT id FROM label where name =?" (eroare!) err = = mdb.Query (s, t.Name) dacă err! = nil return err labelId = -1 pentru rânduri.Next return err dacă labelId == -1 rezultat, err = label_ins.Exec (t.Name) dacă err! = nil retur err labelId, err = result.LastInsertId  rezultat, err = label_song_ins.Exec (labelId, songId) dacă err! = nil return err return zero 

GetSongsByUser () folosește un join + sub-select din user_song referință încrucișată pentru a returna melodii pentru un anumit utilizator. Utilizează Query () metode și apoi scanează mai târziu fiecare rând pentru a popula a Cântec struct din modelul de obiect de domeniu și retur o felie de melodii. Implementarea la nivel inferior ca DB relațional este ascunsă în siguranță.

func (m * InMemoryDataLayer) GetSongsByUser (u Utilizator) ([] Song, eroare) s: = 'SELECT url, titlu, descriere FROM cântec L INNER JOIN user_song UL ON UL.song_id = L.id WHERE UL.user_id = SELECT id de la utilizator WHERE email =?) 'Rânduri, err: = m.db.Query (s, u.Email) dacă err! = Nil return nil, err pentru rows.Next () var song Song err = (de exemplu, songs.Url, & song.Title, & song.Description) dacă err! = nil return nil, err songs = append (melodii, melodie) 

Acesta este un exemplu excelent de utilizare a unei DB relaționale reale, cum ar fi sqlite, pentru implementarea memoriei în memorie în memorie, față de rularea noastră, ceea ce ar necesita păstrarea hărților și asigurarea corectitudinii întreținerii contabile. 

Executarea testelor împotriva SQLite

Acum, că avem un nivel corespunzător de date în memorie, să aruncăm o privire la teste. Am plasat aceste teste într-un pachet separat numit sqlite_test, și importați local stratul de date abstract (modelul de domeniu), stratul de date concret (pentru a crea stratul de date în memorie) și managerul de melodii (codul testat). De asemenea, pregătesc două melodii pentru testele efectuate de artistul panamez El Chombo!

pachet sqlite_test import ("test". "abstract_data_layer". "concrete_data_layer". "song_manager") const (url1 = "https://www.youtube.com/watch?v=MlW7T0SUH0E" url2 = "https: // www. youtube.com/watch?v=cVFDlg4pbwM ") var testSong = Song Url: url1, Nume:" Chacaron " var testSong2 = Song Url: url2, Nume:" El Gato Volador " 

Metodele de testare creează un nou strat de date în memorie pentru a începe de la zero și pot apela acum metode pe stratul de date pentru a pregăti mediul de testare. Atunci când totul este configurat, aceștia pot invoca metodele managerului de melodii și mai târziu verifică dacă stratul de date conține starea așteptată.

De exemplu, AddSong_Success () metoda de testare creează un utilizator, adaugă o melodie folosind managerul de melodii AddSong () metodă și verifică apelul ulterior GetSongsByUser () returnează melodia adăugată. Apoi adaugă o altă melodie și verifică din nou.

Funcția TestAddSong_Success (t * testing.T) u: = Utilizator Nume: "Gigi", Email: "[email protected]" dl, err: = NewInMemoryDataLayer Eroare ("Nu a reușit crearea utilizatorului") lm, err: = NewSongManager (u, dl) în cazul în care err! ! = nil t.Error ("NewSongManager () a returnat" nil "") err = lm.AddSong (testSong, nil) : = dl.GetSongsByUser (u) dacă err! = nil t.Error ("GetSongsByUser () a eșuat") dacă len (melodii)! = 1 t.Error ) dacă melodii [0]! = testSong t.Error ("Cântarea adăugată nu se potrivește cu melodia de intrare") // Adăugați o altă melodie err = lm.AddSong (testSong2, nil) t.Error ("Nu a reușit") melodii, err = dl.GetSongsByUser (u) dacă err! = nil t.Error ("GetSongsByUser () .Error ('GetSongsByUser () nu a returnat două cântece așa cum era de așteptat') dacă melodii [0]! = TestSong t.Error (" nu se potrivește cu cântecul de intrare ") dacă melodiile [1]! = testSong2 t.Error (" Cântarea adăugată nu se potrivește cu piesa de intrare ") 

TestAddSong_Duplicate () metoda de testare este similară, dar în loc să adăugați o melodie nouă pentru a doua oară, ea adaugă aceeași melodie, ceea ce duce la o eroare duplicată a melodiei:

 u: = Utilizator Nume: "Gigi", Email: "[email protected]" dl, err: = NewInMemoryDataLayer () err! = nil t.Error  err = dl.CreateUser (u) dacă err! = nil t.Error ("Nu a reușit crearea utilizatorului") lm, err: = NewSongManager ) err = lm.AddSong (testSong, nil) dacă err! = nil t.Error ("AddSong () a eșuat"), err: = dl.GetSongsByUser ! = nil t.Error ("GetSongsByUser () a eșuat") dacă len (melodii)! = 1 t.Error ('GetSongsByUser () = testSong t.Error ("Cântarea adăugată nu se potrivește cu melodia de intrare") // Adăugați din nou aceeași piesă err = lm.AddSong (testSong, nil) dacă err == nil t.Error (' ar trebui să fi eșuat pentru un cântec duplicat) expectedErrorMsg: = "Cântec duplicat" errorMsg: = err.Error () if errorMsg! = expectedErrorMsg t.Error ('

Concluzie

În acest tutorial am implementat un strat de date în memorie bazat pe SQLite, a populat o bază de date SQLite în memorie cu date de testare și am utilizat stratul de date din memorie pentru a testa aplicația.

În partea a treia, ne vom concentra pe testarea împotriva unui strat local complex de date care constă din mai multe stocări de date (un DB relațional și o cache Redis). Rămâneți aproape.

Cod