Test du code à fort coefficient de données avec Go, partie 2

Vue d'ensemble

Ceci est la partie deux sur cinq d'une série de didacticiels sur le test de code gourmand en données. Dans la première partie, j'ai abordé la conception d'une couche de données abstraites permettant de tester correctement, de gérer les erreurs dans la couche de données, de simuler le code d'accès aux données et de procéder à des tests sur une couche de données abstraite. Dans ce tutoriel, je vais passer en revue les tests avec une véritable couche de données en mémoire basée sur le populaire SQLite.. 

Test par rapport à un magasin de données en mémoire

Le test par rapport à une couche de données abstraites est idéal dans certains cas où vous avez besoin de beaucoup de précision, vous comprenez exactement ce que le code testé va appeler par rapport à la couche de données et la préparation des réponses fictives vous convient..

Parfois, ce n'est pas si facile. La série d'appels à la couche de données peut être difficile à déterminer, ou il faut beaucoup d'efforts pour préparer des réponses prédéfinies appropriées et valides. Dans ces cas, vous devrez peut-être utiliser un magasin de données en mémoire.. 

Les avantages d'un magasin de données en mémoire sont les suivants:

  • C'est très rapide. 
  • Vous travaillez contre un magasin de données réel.
  • Vous pouvez souvent le remplir à partir de zéro en utilisant des fichiers ou du code.

En particulier, si votre magasin de données est une base de données relationnelle, SQLite est une option fantastique. N'oubliez pas qu'il existe des différences entre SQLite et d'autres bases de données relationnelles populaires telles que MySQL et PostgreSQL.

Assurez-vous de tenir compte de cela dans vos tests. Notez que vous accédez toujours à vos données via la couche de données abstraites, mais le magasin de sauvegarde pendant les tests est le magasin de données en mémoire. Votre test remplira les données de test différemment, mais le code à tester ignore parfaitement ce qui se passe..

Utiliser SQLite

SQLite est une base de données intégrée (liée à votre application). Il n'y a pas de serveur de base de données distinct en cours d'exécution. Il stocke généralement les données dans un fichier, mais offre également la possibilité d’utiliser un magasin de sauvegarde en mémoire.. 

Voici la InMemoryDataStore struct. Il fait également partie de la concrete_data_layer package, et il importe le package tiers go-sqlite3 qui implémente l'interface standard "base de données / sql" de Golang.  

package concrete_data_layer import ("database / sql". "abstract_data_layer" _ "github.com/mattn/go-sqlite3" "time" "fmt") tapez InMemoryDataLayer struct db * sql.DB

Construction de la couche de données en mémoire

le NewInMemoryDataLayer () fonction constructeur crée une base de données sqlite en mémoire et renvoie un pointeur sur la InMemoryDataLayer

func NewInMemoryDataLayer () (* (InMemoryDataLayer, erreur) db, err: = sql.Open ("sqlite3", ": memory:") if err! = nil return nil, err err = createSqliteSchema (db) return & InMemoryDataLayer  db, nil 

Notez que chaque fois que vous ouvrez un nouveau ": memory:" DB, vous partez de zéro. Si vous souhaitez une persistance sur plusieurs appels vers NewInMemoryDataLayer (), Tu devrais utiliser fichier :: mémoire:? cache = partagé. Voir ce fil de discussion GitHub pour plus de détails.

le InMemoryDataLayer met en œuvre le Couche de données interface et stocke les données avec les relations correctes dans sa base de données sqlite. Pour ce faire, nous devons d’abord créer un schéma approprié, qui est exactement le travail du createSqliteSchema () fonction dans le constructeur. Il crée trois tables de données - chanson, utilisateur et étiquette - et deux tables de références croisées., label_song et user_song.

Il ajoute des contraintes, des index et des clés étrangères pour relier les tables les unes aux autres. Je ne m'attarderai pas sur les détails spécifiques. L’essentiel est que la totalité du schéma DDL est déclarée comme une seule chaîne (composée de plusieurs instructions DDL) qui sont ensuite exécutées à l’aide de la commande db.Exec () méthode, et si quelque chose ne va pas, il renvoie une erreur. 

func createSqliteSchema (db * sql.DB) erreur schéma: = 'CREATE TABLE SI PAS EXISTE chanson (id INTEGER PRIMARY KEY AUTOINCREMENT, URL TEXTE UNIQUE, nom TEXT, description TEXT); CREATE TABLE IF NOT EXISTS utilisateur (id INTEGER PRIMARY KEY AUTOINCREMENT, nom TEXT, email TEXT UNIQUE, enregistré_at TIMESTAMP, last_login TIMESTAMP); CREATE INDEX user_email_idx ON utilisateur (email); CREATE TABLE SI NOT EXISTS label (id INTEGER PRIMARY KEY AUTOINCREMENT, nom TEXT UNIQUE); CREATE INDEX label_name_idx ON label (nom); CREATE TABLE SI PAS EXISTE label_song (label_id INTEGER NON NULL REFERENCES label (id), song_id INTEGER NON NULL REFERENCES song (id), PRIMARY KEY (label_id, song_id)); CREATE TABLE IF NOT EXISTS user_song (user_id INTEGER NOT NULL REFERENCES utilisateur (id), song_id INTEGER NOT NULL REFERENCES chanson (id), PRIMARY KEY (user_id, song_id)); ' _, err: = db.Exec (schéma) renvoie err 

Il est important de comprendre que bien que SQL soit standard, chaque système de gestion de base de données (SGBD) a sa propre version et que la définition exacte du schéma ne fonctionnera pas nécessairement telle quelle pour une autre base de données..

Implémentation de la couche de données en mémoire

Pour vous donner un aperçu de l’effort de mise en œuvre d’une couche de données en mémoire, voici quelques méthodes: AddSong () et GetSongsByUser ()

le AddSong () méthode fait beaucoup de travail. Il insère un enregistrement dans le chanson table ainsi que dans chacune des tables de référence: label_song et user_song. À chaque étape, si une opération échoue, elle renvoie simplement une erreur. Je n'utilise aucune transaction car elle est conçue uniquement à des fins de test et je ne m'inquiète pas des données partielles dans la base de données..

func (m * InMemoryDataLayer) AddSong (utilisateur, chanson, étiquettes [] Label) error s: = 'INSERT INTO chanson (URL, nom, description) valeurs (?,?,?)' instruction, err: = m .db.Prepare (s) if err! = nil return err résultat, err: = statement.Exec (song.Url, song.Name, song.Description) if err! = nil return err songId, err: = result.LastInsertId () if err! = nil return err s = "SÉLECTIONNER L'ID de l'utilisateur, où email =?" rows, err: = m.db.Query (s, user.Email) si err! = nil renvoyer err var userId int pour rows.Next () err = rows.Scan (& userId) si err! = nil  return err s = 'INSERT INTO user_song (user_id, song_id) valeurs (?,?)', instruction, err = m.db.Prepare (s) si err! = nil retour err _, err = instruction.Exec (userId, songId) if err! = nil return err var IDItiquette int64 s: = "INSERT INTO étiquette (nom) valeurs (?)" étiquette_ins, erreur: = m.db.Prepare (s) si erreur! = nil return err s = 'INSERT INTO label_song (label_id, song_id) valeurs (?,?)' label_song_ins, err: = m.db.Prepare (s) si err! = nil return err pour _, t: = étiquettes de plage s = "SELECT id FROM étiquette où name =?" rows, err: = m.db.Query (s, t.Name) si err! = nil return err labelId = -1 pour rows.Next () err = rows.Scan (& labelId) si err! = nil return err si labelId == -1 résultat, err = label_ins.Exec (t.Name) si err! = nil return err labelId, err = result.LastInsertId () si err! = nil return err  résultat, err = label_song_ins.Exec (labelId, songId) si err! = nil return err return nil 

le GetSongsByUser () utilise une jointure + sous-sélection de la user_song référence croisée pour renvoyer des chansons pour un utilisateur spécifique. Il utilise le Question() méthodes, puis analyse plus tard chaque ligne pour remplir un Chanson struct à partir du modèle d'objet de domaine et renvoie une tranche de chansons. L'implémentation de bas niveau en tant que base de données relationnelle est masquée en toute sécurité.

func (m * InMemoryDataLayer) GetSongsByUser (utilisateur u) ([] Song, error) s: = 'URL choisie, titre, description DE la chanson L INNER JOIN user_song UL SUR UL.song_id = L.id WHERE UL.user_id = SELECT id de l'utilisateur WHERE email =?) 'Rows, err: = m.db.Query (s, u.Email) if err! = Nil return nil, err pour rows.Next () var song Song err = rows.Scan (& song.Url, & song.Title, & song.Description) si err! = nil return nil, err songs = append (chansons, chanson) retour chansons, nil 

C’est un excellent exemple d’utilisation d’une véritable base de données relationnelle, telle que sqlite, pour la mise en place du magasin de données en mémoire, par opposition au roulement du nôtre, ce qui nécessiterait de conserver des cartes et de s’assurer que toute la comptabilité est correcte.. 

Exécuter des tests avec SQLite

Maintenant que nous avons une couche de données en mémoire appropriée, examinons les tests. J'ai placé ces tests dans un paquet séparé appelé sqlite_test, et j'importe localement la couche de données abstraite (le modèle de domaine), la couche de données concrète (pour créer la couche de données en mémoire) et le gestionnaire de chansons (le code à tester). Je prépare également deux chansons de l'artiste sensationnel panaméen El Chombo pour les tests.!

package sqlite_test import ("testing". "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 = Chant Url: url1, Nom:" Chacaron " var testSong2 = Chant Url: url2, Nom:" El Gato Volador " 

Les méthodes de test créent une nouvelle couche de données en mémoire pour démarrer à partir de zéro et peuvent désormais appeler des méthodes sur la couche de données pour préparer l'environnement de test. Une fois que tout est configuré, ils peuvent invoquer les méthodes du gestionnaire de morceaux et vérifier ultérieurement que la couche de données contient l'état attendu..

Par exemple, le AddSong_Success () méthode de test crée un utilisateur, ajoute une chanson à l'aide du gestionnaire de chansons AddSong () méthode, et vérifie que l'appelant plus tard GetSongsByUser () renvoie la chanson ajoutée. Il ajoute ensuite une autre chanson et vérifie à nouveau.

func TestAddSong_Success (t * testing.T) u: = Utilisateur Nom: "Gigi", Email: "[email protected]" dl, err: = NewInMemoryDataLayer () si err! = nil t.Error (" Échec de la création de la couche de données en mémoire ") err = dl.CreateUser (u) si err! = Nil t.Error (" Échec de la création de l'utilisateur ") lm, err: = NewSongManager (u, dl) si err ! = nil t.Error ("NewSongManager () a renvoyé 'nil'") err = lm.AddSong (testSong, nil) si err! = nil t.Error ("AddSong () failed") chansons, err : = dl.GetSongsByUser (u) if err! = nil t.Error ("GetSongsByUser () a échoué") if len (chansons)! = 1 t.Error ('GetSongsByUser () a échoué ") si len (chansons)! = 1 t.Error (' GetSongsByUser () n'a pas renvoyé une chanson comme attendu ') if songs [0]! = testSong t.Error ("La chanson ajoutée ne correspond pas à la chanson saisie") // Ajouter une autre chanson err = lm.AddSong (testSong2, nil) si err! = nil  t.Error ("AddSong () failed") chansons, err = dl.GetSongsByUser (u) si err! = nil t.Error ("GetSongsByUser () échoué") si len (chansons)! = 2 t .Error ('GetSongsByUser () n'a pas renvoyé deux chansons comme prévu') si songs [0]! = TestSong t.Error ("chanson ajoutée ne correspond pas à la chanson entrée ") si chansons [1]! = testSong2 t.Error (" La chanson ajoutée ne correspond pas à la chanson entrée ") 

le TestAddSong_Duplicate () La méthode de test est similaire, mais au lieu d’ajouter une nouvelle chanson la deuxième fois, elle ajoute la même chanson, ce qui entraîne une erreur de duplication:

 u: = Utilisateur Nom: "Gigi", Email: "[email protected]" dl, err: = NewInMemoryDataLayer () si err! = nil t.Error ("Echec de la création de la couche de données en mémoire")  err = dl.CreateUser (u) si err! = nil t.Error ("Impossible de créer un utilisateur") lm, err: = NewSongManager (u, dl) si err! = nil t.Error ("NewSongManager () a retourné 'nil' ") err = lm.AddSong (testSong, nil) si err! = nil t.Error (" AddSong () a échoué ") chansons, err: = dl.GetSongsByUser (u) si err ! = nil t.Error ("GetSongsByUser () a échoué") si len (chansons)! = 1 t.Error ('GetSongsByUser () n'a pas renvoyé une chanson comme prévu') si chansons [0]! = testSong t.Error ("La chanson ajoutée ne correspond pas à la chanson saisie") // Ajoute à nouveau le même morceau err = lm.AddSong (testSong, nil) si err == nil t.Error ('AddSong () aurait dû échouer pour une chanson dupliquée ') attenduErrorMsg: = "Chanson dupliquée" errorMsg: = err.Error () si errorMsg! = attendErrorMsg t.Error (' AddSong () a renvoyé un message d'erreur incorrect pour la chanson dupliquée ')

Conclusion

Dans ce tutoriel, nous avons implémenté une couche de données en mémoire basée sur SQLite, complété une base de données SQLite en mémoire avec des données de test et utilisé la couche de données en mémoire pour tester l'application..

Dans la troisième partie, nous allons nous concentrer sur les tests sur une couche de données complexe locale composée de plusieurs magasins de données (une base de données relationnelle et un cache Redis). Restez à l'écoute.