J'écris un composant qui, étant donné un fichier ZIP, doit:
- Décompressez le fichier.
- Trouvez une DLL spécifique parmi les fichiers décompressés.
- Chargez cette DLL par réflexion et invoquez une méthode dessus.
Je voudrais tester ce composant unitaire.
Je suis tenté d'écrire du code qui traite directement du système de fichiers:
void DoIt()
{
Zip.Unzip(theZipFile, "C:\\foo\\Unzipped");
System.IO.File myDll = File.Open("C:\\foo\\Unzipped\\SuperSecret.bar");
myDll.InvokeSomeSpecialMethod();
}
Mais les gens disent souvent: "N'écrivez pas de tests unitaires qui reposent sur le système de fichiers, la base de données, le réseau, etc."
Si je devais écrire ceci de manière conviviale pour les tests unitaires, je suppose que cela ressemblerait à ceci:
void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
string path = zipper.Unzip(theZipFile);
IFakeFile file = fileSystem.Open(path);
runner.Run(file);
}
Yay! Maintenant, c'est testable; Je peux alimenter en double test (mocks) la méthode DoIt. Mais à quel prix? J'ai maintenant dû définir 3 nouvelles interfaces juste pour rendre cela testable. Et qu'est-ce que je teste exactement? Je teste que ma fonction DoIt interagit correctement avec ses dépendances. Il ne teste pas que le fichier zip a été décompressé correctement, etc.
Je n'ai plus l'impression de tester la fonctionnalité. J'ai l'impression que je teste simplement les interactions de classe.
Ma question est la suivante : quelle est la bonne façon de tester unitaire quelque chose qui dépend du système de fichiers?
edit J'utilise .NET, mais le concept pourrait également appliquer du code Java ou natif.
la source
myDll.InvokeSomeSpecialMethod();
où vous vérifieriez qu'il fonctionne correctement dans les situations de réussite et d'échec, donc je ne ferais pas de test unitaire,DoIt
maisDllRunner.Run
cela dit, une mauvaise utilisation d'un test UNIT pour vérifier que l'ensemble du processus fonctionne serait une mauvaise utilisation acceptable et comme ce serait un test d'intégration masquant un test unitaire, les règles de test unitaire normales n'ont pas besoin d'être appliquées aussi strictementRéponses:
Il n'y a vraiment rien de mal à cela, c'est juste une question de savoir si vous l'appelez un test unitaire ou un test d'intégration. Vous devez simplement vous assurer que si vous interagissez avec le système de fichiers, il n'y a pas d'effets secondaires indésirables. Plus précisément, assurez-vous que vous nettoyez après vous-même - supprimez tous les fichiers temporaires que vous avez créés - et que vous n'écrasez pas accidentellement un fichier existant qui portait le même nom de fichier qu'un fichier temporaire que vous utilisiez. Utilisez toujours des chemins relatifs et non des chemins absolus.
Ce serait également une bonne idée de placer
chdir()
dans un répertoire temporaire avant d'exécuter votre test, et dechdir()
revenir ensuite.la source
chdir()
concerne l'ensemble du processus, vous risquez donc de ne pas pouvoir exécuter vos tests en parallèle, si votre cadre de test ou une version future de celui-ci le prend en charge.Vous avez frappé le clou juste sur sa tête. Ce que vous voulez tester, c'est la logique de votre méthode, pas nécessairement si un vrai fichier peut être adressé. Vous n'avez pas besoin de tester (dans ce test unitaire) si un fichier est correctement décompressé, votre méthode prend cela pour acquis. Les interfaces sont précieuses en elles-mêmes car elles fournissent des abstractions sur lesquelles vous pouvez programmer, plutôt que de vous fier implicitement ou explicitement à une implémentation concrète.
la source
DoIt
fonction testable comme indiqué n'a même pas besoin d'être testée. Comme l'intervenant l'a souligné à juste titre, il ne reste plus rien d'important à tester. Maintenant, c'est la mise en œuvre deIZipper
,IFileSystem
etIDllRunner
cela doit être testé, mais ce sont précisément les choses qui ont été simulées pour le test!Votre question expose l'une des parties les plus difficiles des tests pour les développeurs qui ne font que commencer:
"Qu'est-ce que je teste?"
Votre exemple n'est pas très intéressant car il rassemble simplement certains appels d'API, donc si vous deviez écrire un test unitaire pour celui-ci, vous finiriez par affirmer que les méthodes ont été appelées. Des tests comme celui-ci associent étroitement les détails de votre implémentation au test. C'est mauvais car maintenant vous devez changer le test à chaque fois que vous changez les détails d'implémentation de votre méthode parce que changer les détails d'implémentation casse vos tests!
Avoir de mauvais tests est en fait pire que de ne pas avoir de tests du tout.
Dans votre exemple:
Bien que vous puissiez passer des simulations, il n'y a pas de logique dans la méthode à tester. Si vous tentez un test unitaire pour cela, cela pourrait ressembler à ceci:
Félicitations, vous avez essentiellement copié-collé les détails d'implémentation de votre
DoIt()
méthode dans un test. Bon maintien.Lorsque vous écrivez des tests, vous voulez tester le QUOI et non le COMMENT . Voir Black Box Testing pour en savoir plus.
Le WHAT est le nom de votre méthode (ou du moins il devrait l'être). Le HOW sont tous les petits détails d'implémentation qui vivent à l'intérieur de votre méthode. De bons tests vous permettent d'échanger le COMMENT sans casser le QUOI .
Pensez-y de cette façon, demandez-vous:
"Si je change les détails de mise en œuvre de cette méthode (sans modifier le marché public), est-ce que cela va casser mon (mes) test (s)?"
Si la réponse est oui, vous testez le COMMENT et non le QUOI .
Pour répondre à votre question spécifique sur le test du code avec les dépendances du système de fichiers, disons que vous aviez quelque chose d'un peu plus intéressant avec un fichier et que vous vouliez enregistrer le contenu encodé en Base64 d'un
byte[]
dans un fichier. Vous pouvez utiliser des flux pour cela pour tester que votre code fait la bonne chose sans avoir à vérifier comment il le fait. Un exemple pourrait être quelque chose comme ceci (en Java):Le test utilise un
ByteArrayOutputStream
mais dans l'application (par injection de dépendance) la vraie StreamFactory (peut - être appelé FileStreamFactory) retourneraitFileOutputStream
deoutputStream()
et écrirait à unFile
.Ce qui était intéressant à propos de la
write
méthode ici, c'est qu'elle écrivait le contenu encodé en Base64, c'est donc ce que nous avons testé. Pour votreDoIt()
méthode, cela serait mieux testé avec un test d'intégration .la source
Je suis réticent à polluer mon code avec des types et des concepts qui n'existent que pour faciliter les tests unitaires. Bien sûr, si cela rend le design plus propre et meilleur, c'est génial, mais je pense que ce n'est souvent pas le cas.
Mon point de vue est que vos tests unitaires feraient tout ce qu'ils peuvent, ce qui peut ne pas être une couverture à 100%. En fait, ce n'est peut-être que 10%. Le fait est que vos tests unitaires doivent être rapides et ne comporter aucune dépendance externe. Ils peuvent tester des cas comme "cette méthode lève une ArgumentNullException lorsque vous passez null pour ce paramètre".
J'ajouterais ensuite des tests d'intégration (également automatisés et probablement utilisant le même cadre de test unitaire) qui peuvent avoir des dépendances externes et tester des scénarios de bout en bout tels que ceux-ci.
Lors de la mesure de la couverture de code, je mesure à la fois des tests unitaires et d'intégration.
la source
Il n'y a rien de mal à frapper le système de fichiers, considérez-le simplement comme un test d'intégration plutôt qu'un test unitaire. J'échangerais le chemin codé en dur avec un chemin relatif et créerais un sous-dossier TestData pour contenir les zips pour les tests unitaires.
Si vos tests d'intégration prennent trop de temps à s'exécuter, séparez-les afin qu'ils ne s'exécutent pas aussi souvent que vos tests unitaires rapides.
Je suis d'accord, parfois je pense que les tests basés sur l'interaction peuvent causer trop de couplage et finissent souvent par ne pas fournir suffisamment de valeur. Vous voulez vraiment tester la décompression du fichier ici, pas seulement vérifier que vous appelez les bonnes méthodes.
la source
Une façon serait d'écrire la méthode unzip pour prendre InputStreams. Ensuite, le test unitaire pourrait construire un tel InputStream à partir d'un tableau d'octets en utilisant ByteArrayInputStream. Le contenu de ce tableau d'octets peut être une constante dans le code de test unitaire.
la source
Cela semble être plus un test d'intégration car vous dépendez d'un détail spécifique (le système de fichiers) qui pourrait changer, en théorie.
Je résumerais le code qui traite du système d'exploitation dans son propre module (classe, assembly, jar, peu importe). Dans votre cas, vous souhaitez charger une DLL spécifique si elle est trouvée, alors créez une interface IDllLoader et une classe DllLoader. Demandez à votre application d'acquérir la DLL du DllLoader en utilisant l'interface et de tester cela .. vous n'êtes pas responsable du code de décompression après tout, n'est-ce pas?
la source
En supposant que les «interactions du système de fichiers» sont bien testées dans le cadre lui-même, créez votre méthode pour travailler avec les flux et testez-la. Ouvrir un FileStream et le transmettre à la méthode peut être omis de vos tests, car FileStream.Open est bien testé par les créateurs du framework.
la source
Vous ne devez pas tester l'interaction de classe et l'appel de fonction. à la place, vous devriez envisager des tests d'intégration. Testez le résultat requis et non l'opération de chargement de fichier.
la source
Pour le test unitaire, je vous suggère d'inclure le fichier de test dans votre projet (fichier EAR ou équivalent) puis d'utiliser un chemin relatif dans les tests unitaires, c'est-à-dire "../testdata/testfile".
Tant que votre projet est correctement exporté / importé, votre test unitaire devrait fonctionner.
la source
Comme d'autres l'ont dit, le premier est parfait comme test d'intégration. Le second teste uniquement ce que la fonction est censée faire réellement, ce qui est tout ce qu'un test unitaire devrait faire.
Comme indiqué, le deuxième exemple semble un peu inutile, mais il vous donne la possibilité de tester la façon dont la fonction répond aux erreurs dans l'une des étapes. Vous n'avez aucune vérification d'erreur dans l'exemple, mais dans le système réel, vous pouvez avoir, et l'injection de dépendances vous permettrait de tester toutes les réponses à toutes les erreurs. Ensuite, le coût en aura valu la peine.
la source