Ceci est la partie cinq sur cinq d'une série de tutoriels sur le test de code gourmand en données. Dans la quatrième partie, j'ai couvert les magasins de données distants, en utilisant des bases de données de test partagées, en utilisant des instantanés de données de production et en générant vos propres données de test. Dans ce tutoriel, je vais passer en revue les tests fuzz, tester votre cache, tester l'intégrité des données, tester l'idempotency et les données manquantes..
L'idée de tester le fuzz est de submerger le système avec beaucoup d'entrées aléatoires. Au lieu d'essayer de penser à une entrée qui couvrira tous les cas, ce qui peut être difficile et / ou très laborieux, vous laissez la chance le faire pour vous. Il est conceptuellement similaire à la génération de données aléatoires, mais l'intention est ici de générer des entrées aléatoires ou semi-aléatoires plutôt que des données persistantes..
Le test Fuzz est particulièrement utile pour détecter des problèmes de sécurité et de performances lorsque des entrées inattendues provoquent des pannes ou des fuites de mémoire. Mais cela peut aussi aider à s'assurer que toutes les entrées non valides sont détectées tôt et sont correctement rejetées par le système..
Prenons, par exemple, une entrée qui se présente sous la forme de documents JSON profondément imbriqués (très fréquents dans les API Web). Essayer de générer manuellement une liste complète de cas de test est à la fois source d'erreurs et de travail. Mais le test du fuzz est la technique parfaite.
Il existe plusieurs bibliothèques que vous pouvez utiliser pour tester fuzz. Mon préféré est gofuzz de Google. Voici un exemple simple qui génère automatiquement 200 objets uniques d’une structure à plusieurs champs, y compris une structure imbriquée..
import ("fmt" "github.com/google/gofuzz") func SimpleFuzzing () type SomeType struct Une chaîne B chaîne C int D structure E float64 f: = fuzz.New () objet: = SomeType uniqueObjects: = map [SomeType] int pour i: = 0; je < 200; i++ f.Fuzz(&object) uniqueObjects[object]++ fmt.Printf("Got %v unique objects.\n", len(uniqueObjects)) // Output: // Got 200 unique objects.
Presque tous les systèmes complexes qui traitent beaucoup de données ont un cache, ou plus probablement plusieurs niveaux de caches hiérarchiques. Comme dit le proverbe, il n’ya que deux choses difficiles en informatique: nommer des choses, invalider la mémoire cache et commettre une erreur erronée.
Blague à part, la gestion de votre stratégie de mise en cache et de sa mise en œuvre peut compliquer votre accès aux données, mais elle a également un impact considérable sur les coûts et les performances de votre accès aux données. Le test de votre cache ne peut pas être effectué de l'extérieur car votre interface cache l'origine des données et le mécanisme de cache est un détail de la mise en œuvre..
Voyons comment tester le comportement en cache de la couche de données hybride Songify.
Les caches vivent et meurent grâce à leurs performances de hit / miss. La fonctionnalité de base d'un cache est que si les données demandées sont disponibles dans le cache (un hit), elles seront extraites du cache et non du magasin de données principal. Dans la conception originale du HybridDataLayer
, l'accès au cache s'est fait par des méthodes privées.
Les règles de visibilité rendent impossible leur appel direct ou leur remplacement à partir d'un autre package. Pour activer les tests de cache, je vais changer ces méthodes en fonctions publiques. C’est très bien, car le code de l’application réelle fonctionne par le biais du Couche de données
interface, qui n'expose pas ces méthodes.
Le code de test pourra toutefois remplacer ces fonctions publiques si nécessaire. Premièrement, ajoutons une méthode pour accéder au client Redis, afin de pouvoir manipuler le cache:
func (m * HybridDataLayer) GetRedis () * redis.Client return m.redis
Ensuite, je vais changer le getSongByUser_DB ()
méthodes à une variable de fonction publique. Maintenant, dans le test, je peux remplacer le GetSongsByUser_DB ()
variable avec une fonction qui garde la trace de combien de fois il a été appelé, puis le transmet à la fonction originale. Cela nous permet de vérifier si un appel à GetSongsByUser ()
récupéré les morceaux du cache ou de la base de données.
Décomposons cela pièce par pièce. Premièrement, nous obtenons la couche de données (qui efface également la base de données et les redis), créons un utilisateur et ajoutons une chanson. le AddSong ()
méthode remplit également redis.
func TestGetSongsByUser_Cache (t * testing.T) maintenant: = heure.Maintenant () u: = utilisateur nom: "Gigi", email: "[email protected]", RegisteredAt: maintenant, LastLogin: now dl, err : = getDataLayer () si err! = nil t.Error ("Impossible de créer une couche de données hybride") err = dl.CreateUser (u) si err! = nil t.Error ("Échec de la création d'un 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 () a échoué")
C'est la partie cool. Je garde la fonction d'origine et définit une nouvelle fonction instrumentée qui incrémente le local callCount
variable (tout est dans une fermeture) et appelle la fonction d'origine. Ensuite, j'assigne la fonction instrumentée à la variable GetSongsByUser_DB
. Désormais, chaque appel de la couche de données hybride à GetSongsByUser_DB ()
ira à la fonction instrumentée.
callCount: = 0 originalFunc: = GetSongsByUser_DB instrumentedFunc: = func (m * HybridDataLayer, chaîne de courrier électronique, chansons * [] Song) (erreur d'erreur) callCount + = 1 retourné originalFunc (m, email, chansons) GetSongsByUser_DB = instrumentedFunc
À ce stade, nous sommes prêts à tester le fonctionnement du cache. Tout d’abord, le test appelle le GetSongsByUser ()
du SongManager
qui le transmet à la couche de données hybride. Le cache est censé être rempli pour cet utilisateur que nous venons d'ajouter. Donc, le résultat attendu est que notre fonction instrumentée ne sera pas appelée, et le callCount
restera à zéro.
_, err = lm.GetSongsByUser (u) si err! = nil t.Error ("GetSongsByUser () failed") // Vérifiez que la base de données n'a pas été utilisée car le cache doit être // rempli par AddSong () si callCount > 0 t.Error ('GetSongsByUser_DB () appelé alors qu'il ne devrait pas en avoir')
Le dernier cas de test consiste à s'assurer que si les données de l'utilisateur ne se trouvent pas dans le cache, elles seront récupérées correctement à partir de la base de données. Le test le permet en vidant Redis (en effaçant toutes ses données) et en effectuant un autre appel à GetSongsByUser ()
. Cette fois, la fonction instrumentée sera appelée et le test vérifie que le callCount
est égal à 1. Enfin, l'original GetSongsByUser_DB ()
la fonction est restaurée.
// Efface le cache dl.GetRedis (). FlushDB () // Récupère les chansons à présent, il devrait maintenant aller à la base de données // car le cache est vide _, err = lm.GetSongsByUser (u) if err! = Nil t.Error ("GetSongsByUser () a échoué") // Vérifiez que la base de données a été utilisée, car le cache est vide si callCount! = 1 t.Error ('GetSongsByUser_DB () n'a pas été appelé comme il aurait dû ") GetSongsByUser_DB = originalFunc
Notre cache est très basique et ne fait aucune invalidation. Cela fonctionne plutôt bien tant que toutes les chansons sont ajoutées à travers le AddSong ()
méthode qui prend soin de mettre à jour Redis. Si nous ajoutons plus d'opérations telles que la suppression de chansons ou la suppression d'utilisateurs, ces opérations doivent permettre de mettre à jour Redis en conséquence..
Ce cache très simple fonctionnera même si nous avons un système distribué sur lequel plusieurs machines indépendantes peuvent exécuter notre service Songify, à condition que toutes les instances fonctionnent avec les mêmes instances de base de données et Redis..
Toutefois, si la base de données et le cache peuvent ne plus être synchronisés en raison d'opérations de maintenance ou d'autres outils et applications modifiant les données, nous devons définir une stratégie d'invalidation et d'actualisation du cache. Il peut être testé en utilisant les mêmes techniques: remplacer les fonctions cible ou accéder directement à la base de données et à Redis lors de votre test pour vérifier l'état..
Habituellement, vous ne pouvez pas laisser le cache se développer à l'infini. Un système commun pour conserver les données les plus utiles dans le cache est le cache LRU (le moins récemment utilisé). Les données les plus anciennes sont éjectées du cache lorsqu'elles atteignent leur capacité maximale.
Le tester implique de définir la capacité sur un nombre relativement petit pendant le test, de dépasser la capacité et de s'assurer que les données les plus anciennes ne sont plus dans le cache et que leur accès nécessite un accès à une base de données..
Votre système dépend de l’intégrité de vos données. Si vous avez des données corrompues ou des données manquantes, alors vous êtes en mauvais état. Dans les systèmes réels, il est difficile de maintenir une intégrité des données parfaite. Les schémas et les formats changent, les données sont ingérées via des canaux qui ne vérifient peut-être pas toutes les contraintes, les bogues laissés dans des données incorrectes, les tentatives des administrateurs qui tentent des corrections manuelles, les sauvegardes et les restaurations peuvent ne pas être fiables.
Face à cette dure réalité, vous devez tester l'intégrité des données de votre système. Tester l'intégrité des données est différent des tests automatisés réguliers après chaque changement de code. La raison en est que les données peuvent aller mal même si le code n'a pas changé. Vous souhaitez certainement exécuter des contrôles d'intégrité des données après des modifications de code susceptibles de modifier le stockage ou la représentation des données, mais également les exécuter régulièrement.
Les contraintes sont le fondement de votre modélisation de données. Si vous utilisez une base de données relationnelle, vous pouvez définir des contraintes au niveau SQL et laisser la base de données les appliquer. La nullité, la longueur des champs de texte, l'unicité et les relations 1-N peuvent être définies facilement. Mais SQL ne peut pas vérifier toutes les contraintes.
Par exemple, dans Desongcious, il existe une relation N-N entre les utilisateurs et les chansons. Chaque chanson doit être associée à au moins un utilisateur. Il n’existe aucun moyen efficace d’imposer cela dans SQL (vous pouvez avoir une clé étrangère d’un morceau à l’autre et faire pointer ce morceau vers l’un des utilisateurs qui lui sont associés). Une autre contrainte peut être que chaque utilisateur puisse avoir au plus 500 chansons. Encore une fois, il n’ya aucun moyen de le représenter en SQL. Si vous utilisez des magasins de données NoSQL, la déclaration et la validation des contraintes au niveau du magasin de données sont généralement encore moins prises en charge..
Cela vous laisse quelques options:
Idempotency signifie qu'exécuter la même opération plusieurs fois de suite aura le même effet que de l'exécuter une fois..
Par exemple, définir la variable x sur 5 est idempotent. Vous pouvez définir x à 5 une fois ou un million de fois. Ce sera toujours 5. Cependant, incrémenter X de 1 n'est pas idempotent. Chaque incrément consécutif change de valeur. Idempotency est une propriété très souhaitable dans les systèmes distribués avec des partitions réseau temporaires et des protocoles de récupération qui réessayent d'envoyer un message plusieurs fois s'il n'y a pas de réponse immédiate..
Si vous indiquez idempotency dans votre code d'accès aux données, vous devez le tester. Ceci est généralement très facile. Pour chaque opération idempotente que vous étendez, effectuez l'opération deux fois ou plus sur une ligne et vérifiez qu'il n'y a pas d'erreur et que l'état reste le même..
Notez que la conception idempotente peut parfois masquer des erreurs. Envisagez de supprimer un enregistrement d'une base de données. C'est une opération idempotente. Une fois que vous avez supprimé un enregistrement, celui-ci n'existe plus dans le système et toute tentative de le supprimer à nouveau ne le ramènera pas. Cela signifie que tenter de supprimer un enregistrement inexistant est une opération valide. Mais cela pourrait masquer le fait que l'appelant a passé la mauvaise clé d'enregistrement. Si vous retournez un message d'erreur alors ce n'est pas idempotent.
Les migrations de données peuvent être des opérations très risquées. Parfois, vous exécutez un script sur toutes vos données ou des parties critiques de vos données et effectuez une opération chirurgicale sérieuse. Vous devez être prêt avec le plan B en cas de problème (par exemple, retournez aux données d'origine et déterminez ce qui s'est mal passé)..
Dans de nombreux cas, la migration de données peut être une opération lente et coûteuse pouvant nécessiter la mise en parallèle de deux systèmes pendant la durée de la migration. J'ai participé à plusieurs migrations de données qui ont pris plusieurs jours, voire plusieurs semaines. Face à une migration massive de données, il vaut la peine d'investir du temps et de tester la migration elle-même sur un petit sous-ensemble (mais représentatif) de vos données, puis de vérifier que les données nouvellement migrées sont valides et que le système peut les utiliser..
Le manque de données est un problème intéressant. Parfois, des données manquantes violent l’intégrité de vos données (par exemple, une chanson dont l’utilisateur est manquant) et parfois, il manque tout simplement (par exemple, une personne supprime un utilisateur et toutes ses chansons)..
Si les données manquantes posent un problème d'intégrité des données, vous les détecterez dans vos tests d'intégrité des données. Toutefois, si certaines données manquent, il n’existe aucun moyen facile de les détecter. Si les données ne sont jamais entrées dans le stockage persistant, il se peut qu'il y ait une trace dans les journaux ou dans d'autres magasins temporaires..
En fonction du risque de données manquantes, vous pouvez écrire des tests qui suppriment délibérément certaines données de votre système et vérifient que celui-ci se comporte comme prévu..
Tester un code gourmand en données nécessite une planification délibérée et une compréhension de vos exigences de qualité. Vous pouvez effectuer des tests à plusieurs niveaux d'abstraction. Vos choix influeront sur la précision et la complexité de vos tests, sur le nombre d'aspects de votre couche de données que vous testez, sur la rapidité d'exécution de vos tests et sur la facilité de modification de vos tests lorsque vous le testez. changements de couche de données.
Il n'y a pas une seule bonne réponse. Vous devez trouver votre cible idéale parmi les tests très complets, lents et laborieux, en passant par les tests rapides et légers..