Comment appliquer TDD aux fonctions de lecture / écriture?

10

Cela semble être un problème de poulet et d'oeuf.

Vous pouvez faire écrire une fonction d'écriture dans un magasin de données, mais ne savez jamais que vous l'avez enregistrée correctement sans fonction de lecture testée.

Vous pouvez faire lire une fonction de lecture à partir d'un magasin de données, mais comment mettre des choses dans ce magasin de données, à lire, sans fonction d'écriture testée?

ÉDITER:

Je me connecte et effectue des transactions avec une base de données SQL pour enregistrer et charger des objets à utiliser. Il n'y a aucun intérêt à tester les fonctions d'accès fournies par la base de données, mais j'encapsule ces fonctions de base de données pour sérialiser / désérialiser les objets. Je veux être sûr d'écrire et de lire correctement les bonnes choses vers et depuis la base de données.

Ce n'est pas comme ajouter / supprimer, comme le mentionne @snowman. Je veux savoir que le contenu que j'ai écrit est correct, mais cela nécessite une fonction de lecture bien testée. Quand je lis, je veux être sûr que ma lecture a correctement créé un objet égal à ce qui a été écrit; mais cela nécessite une fonction d'écriture bien testée.

user2738698
la source
Ecrivez-vous votre propre magasin de données ou utilisez-vous un magasin existant? Si vous en utilisez un existant, supposez qu'il fonctionne déjà. Si vous écrivez le vôtre, cela fonctionne de la même manière que l'écriture de tout autre logiciel à l'aide de TDD.
Robert Harvey
1
Bien que étroitement lié, je ne pense pas qu'il s'agisse d'un double de cette question particulière. La cible dupe parle d'ajout / suppression, celle-ci est en lecture / écriture. La différence est qu'une dépendance en lecture / écriture repose probablement sur le contenu des objets en cours de lecture / écriture, tandis qu'un simple test d'ajout / suppression serait probablement beaucoup plus simple: l'objet existe-t-il ou non?
2
Vous pouvez mettre en scène une base de données avec des données et aucune fonction d'écriture pour tester une fonction de lecture.
JeffO

Réponses:

7

Commencez avec la fonction de lecture.

  • Dans la configuration de test : créez la base de données et ajoutez les données de test. soit via des scripts de migration, soit à partir d'une sauvegarde. Comme ce n'est pas votre code, il ne nécessite pas de test en TDD

  • Dans le test : instanciez votre référentiel, pointez sur votre base de données de test et appelez la méthode Read. Vérifiez que les données de test sont renvoyées.

Maintenant que vous avez une fonction de lecture entièrement testée, vous pouvez passer à la fonction d'écriture, qui peut utiliser la lecture existante pour vérifier ses propres résultats

Ewan
la source
Je suppose que vous pourriez créer une base de données en mémoire pour accélérer les choses, mais cela pourrait être trop complexe. Pourquoi ne pas utiliser des simulateurs à la place dans les tests unitaires?
BЈовић
1
Un peu de Devil's Advocate ici, mais comment testez-vous que la base de données a été créée correctement? Comme OP l'a dit, poulet et œuf.
user949300
1
@Ewan - Je suis absolument d'accord que vous ne devriez pas tester leur code DB. Mais comment savez-vous que votre code de configuration DB n'a pas oublié un INSERT quelque part ou mis une mauvaise valeur dans une colonne?
user949300
1
à partir d'une approche TDD pure, le test EST l'exigence. il ne peut donc logiquement pas se tromper. obvs dans le monde réel, vous devez le regarder
Ewan
1
Quis custodiet ipsos custodes? Ou: "Qui teste les tests?" :-) Je suis d'accord avec vous que dans un monde TDD puriste, ce serait la façon horriblement fastidieuse et sujette aux bogues (surtout s'il s'agissait d'une structure à plusieurs tables compliquée avec 8 JOINS) de le faire. A voté.
user949300
6

Je fais souvent juste une écriture suivie d'une lecture. par exemple (pseudocode)

Foo foo1 = setup some object to write
File tempfile = create a tempfile, possibly in memory 
writeFoo(foo1, tempfile) 
Foo foo2 = readFoo(tempfile) 
assertEquals(foo1, foo2); 
clean-up goes here

Ajouté plus tard

En plus que cette solution soit "prgamatique" et "assez bonne", on pourrait soutenir que les autres solutions testent la mauvaise chose . Tester si les chaînes ou les instructions SQL correspondent n'est pas une idée terrible, je l'ai fait moi-même, mais cela teste un effet secondaire et est fragile. Que faire si vous modifiez la capitalisation, ajoutez un champ ou mettez à jour un numéro de version dans vos données? Que faire si votre pilote SQL change l'ordre des appels pour plus d'efficacité ou si votre sérialiseur XML mis à jour ajoute un espace supplémentaire ou modifie une version de schéma?

Maintenant, si vous devez respecter très strictement certaines spécifications officielles, je conviens que la vérification des détails est appropriée.

user949300
la source
1
Parce que c'est un pseudocode vraiment dense à 90%? Pas certain. Peut-être mettre en surbrillance le texte et rendre le code moins bruyant?
RubberDuck
1
Oui @Ewan. Le fanatique froncerait les sourcils à ce sujet, mais le programmeur pragmatique dirait "assez bien" et passer à autre chose.
RubberDuck
1
je lis un peu la question comme ... "en supposant que je suis TDD comme un fanatique ..."
Ewan
1
mon interprétation de l'OP et du TDD est que votre test doit être écrit en premier et ne pas utiliser à la fois la lecture et l'écriture, à moins que l'un ne soit également testé ailleurs.
Ewan
2
vous testez, 'read should return what i write' mais les exigences sont 'read should return data from the db' et 'write should write data to the db'
Ewan
4

Non. Ne pas tester les E / S unitaires. C'est une perte de temps.

Logique de test unitaire. S'il y a beaucoup de logique que vous voulez tester dans le code d'E / S, vous devez refactoriser votre code pour séparer la logique de la façon dont vous effectuez les E / S et ce que vous faites des E / S de l'activité réelle de faire des E / S (qui est quasiment impossible à tester).

Pour élaborer un peu, si vous souhaitez tester un serveur HTTP, vous devez le faire à travers deux types de tests: les tests d'intégration et les tests unitaires. Les tests unitaires ne doivent pas du tout interagir avec les E / S. C'est lent et introduit de nombreuses conditions d'erreur qui n'ont rien à voir avec l'exactitude de votre code. Les tests unitaires ne doivent pas être soumis à l'état de votre réseau!

Votre code doit séparer:

  • La logique de déterminer quelles informations envoyer
  • La logique de détermination des octets à envoyer afin d'envoyer un bit d'information particulier (comment puis-je coder une réponse, etc. en octets bruts), et
  • Le mécanisme d'écriture de ces octets dans une socket.

Les deux premiers impliquent la logique et les décisions et nécessitent des tests unitaires. Le dernier n'implique pas de prendre de nombreuses décisions, le cas échéant, et peut être testé à merveille à l'aide de tests d'intégration.

En fait, c'est juste un bon design en général, mais l'une des raisons en est qu'il facilite le test.


Voici quelques exemples:

  • Si vous écrivez du code qui obtient des données d'une base de données relationnelle, vous pouvez tester de façon unitaire comment vous mappez les données renvoyées par les requêtes relationnelles à votre modèle d'application.
  • Si vous écrivez du code qui écrit des données dans une base de données relationnelle, vous pouvez tester unitairement les éléments de données que vous souhaitez écrire dans la base de données sans réellement tester les requêtes SQL particulières que vous utilisez. Par exemple, vous pouvez conserver deux copies de l'état de votre application en mémoire: une copie représentant à quoi ressemble la base de données et la copie de travail. Lorsque vous souhaitez vous synchroniser avec la base de données, vous devez les différencier et écrire les différences dans la base de données. Vous pouvez très facilement tester de façon unitaire ce code diff.
  • Si vous écrivez du code qui lit quelque chose dans un fichier de configuration, vous souhaitez tester votre analyseur de format de fichier de configuration, mais avec des chaînes de votre fichier source de test plutôt que des chaînes que vous obtenez à partir du disque.
Miles Rout
la source
2

Je ne sais pas si c'est une pratique standard ou non, mais cela fonctionne bien pour moi.

Dans mes implémentations de méthodes de lecture / écriture non basées sur une base de données, j'utilise mes propres méthodes toString()et fromString()méthodes spécifiques au type comme détails d'implémentation.

Ceux-ci peuvent être facilement testés isolément:

 assertEquals("<xml><car type='porsche'>....", new Car("porsche").toString());

Pour les méthodes de lecture et d'écriture, j'ai un test d'intégration qui lit et écrit physiquement en un seul test

Au fait: y a-t-il quelque chose de mal à avoir un test qui teste la lecture / écriture ensemble?

k3b
la source
Cela peut ne pas sembler bon ou "pur", mais c'est la solution pragmatique.
RubberDuck
J'aime aussi l'idée de tester la lecture et l'écriture ensemble. Votre toString () est un joli compromis pragmatique.
user949300
1

Les données connues doivent être formatées de manière connue. La manière la plus simple de l'implémenter est d'utiliser une chaîne constante et de comparer le résultat, comme décrit @ k3b.

Vous n'êtes cependant pas limité aux constantes. Il peut y avoir un certain nombre de propriétés des données écrites que vous pouvez extraire à l'aide d'un autre type d'analyseur, comme des expressions régulières ou même des sondes ad hoc à la recherche de fonctionnalités des données.

En ce qui concerne la lecture ou l'écriture des données, il peut être utile d'avoir un système de fichiers en mémoire qui vous permet d'exécuter vos tests sans possibilité d'interférence d'autres parties du système. Si vous n'avez pas accès à un bon système de fichiers en mémoire, utilisez une arborescence de répertoires temporaire.

BobDalgleish
la source
1

Utilisez l'injection de dépendance et la moquerie.

Vous ne voulez pas tester votre pilote SQL et vous ne voulez pas tester si votre base de données SQL est en ligne et configurée correctement. Cela ferait partie d'un test d'intégration ou de système. Vous voulez tester si votre code envoie les instructions SQL qu'il est censé envoyer et s'il interprète les réponses comme il est censé le faire.

Donc, lorsque vous avez une méthode / classe qui est censée faire quelque chose avec une base de données, ne lui demandez pas d'obtenir cette connexion de base de données par elle-même. Modifiez-le afin que l'objet qui représente la connexion à la base de données lui soit transmis.

Dans votre code de production, passez l'objet de base de données réel.

Dans vos tests unitaires, passez un objet factice qui se comporte comme si une base de données réelle ne contactait pas réellement un serveur de base de données. Demandez-lui simplement de vérifier s'il reçoit les instructions SQL qu'il est censé recevoir, puis répond avec des réponses codées en dur.

De cette façon, vous pouvez tester votre couche d'abstraction de base de données sans même avoir besoin d'une base de données réelle.

Philipp
la source
Devil's Advocate: Comment savez-vous quelles instructions SQL il est "censé recevoir"? Que faire si le pilote DB optimise l'ordre à partir de ce qui apparaît dans le code?
user949300
@ user949300 Un objet factice de base de données remplace généralement le pilote de base de données.
Philipp
lorsque vous testez un référentiel, il est inutile d'injecter un client de base de données simulé. Vous devez tester que votre code exécute sql qui fonctionne sur la base de données. sinon vous finissez par tester simplement votre maquette
Ewan
@Ewan Ce n'est pas de cela qu'il s'agit. Un test unitaire teste une unité de code, isolée du reste du monde. Vous ne testez pas les interactions entre les composants, comme votre code et la base de données. C'est à cela que servent les tests d'intégration.
Philipp
Oui. im disant theres aucune unité de point testant un référentiel db. test d'intégration est la seule chose qui vaut la peine d'être faite
Ewan
0

Si vous utilisez un mappeur relationnel objet, il existe généralement une bibliothèque associée qui peut être utilisée pour tester le bon fonctionnement de vos mappages en créant un agrégat, en le conservant et en le rechargeant à partir d'une nouvelle session, suivie d'une vérification de l'état par rapport à l'objet d'origine.

NHibernate propose des tests de spécification de persistance . Il peut être configuré pour fonctionner avec un magasin en mémoire pour des tests unitaires rapides.

Si vous suivez la version la plus simple des modèles de référentiel et d'unité de travail et testez tous vos mappages, vous pouvez compter sur des choses qui fonctionnent à peu près.

pnschofield
la source