Je suis assez nouveau dans le monde des tests unitaires et je viens de décider d'ajouter la couverture de test pour mon application existante cette semaine.
C'est une tâche énorme, principalement à cause du nombre de classes à tester mais aussi parce que l'écriture de tests est toute nouvelle pour moi.
J'ai déjà écrit des tests pour plusieurs classes, mais maintenant je me demande si je le fais bien.
Lorsque j'écris des tests pour une méthode, j'ai le sentiment de réécrire une seconde fois ce que j'ai déjà écrit dans la méthode elle-même.
Mes tests semblent si étroitement liés à la méthode (tester tout le chemin de code, s'attendre à ce que certaines méthodes internes soient appelées un certain nombre de fois, avec certains arguments), de sorte qu'il semble que si je refactorise la méthode, les tests échouera même si le comportement final de la méthode n'a pas changé.
Ceci est juste un sentiment, et comme dit plus tôt, je n'ai aucune expérience de test. Si des testeurs plus expérimentés pouvaient me donner des conseils sur la manière de rédiger d'excellents tests pour une application existante, ce serait grandement apprécié.
Edit: j'aimerais remercier Stack Overflow. En moins de 15 minutes, j’ai eu d'excellentes contributions qui ont répondu à plus d'heures de lecture en ligne que je viens de faire.
Mes tests semblent si étroitement liés à la méthode (tester tout le chemin de code, prévoyant que certaines méthodes internes soient appelées un certain nombre de fois, avec certains arguments), qu'il semble que si je refacture la méthode, les tests échoueront même si le comportement final de la méthode n'a pas changé.
Je pense que vous le faites mal.
Un test unitaire devrait:
Il ne faut pas regarder à l'intérieur de la méthode pour voir ce qu'elle fait, donc changer les internes ne devrait pas faire échouer le test. Vous ne devez pas vérifier directement que des méthodes privées sont appelées. Si vous souhaitez savoir si votre code privé est testé, utilisez un outil de couverture de code. Mais ne soyez pas obsédé par cela: une couverture de 100% n'est pas une exigence.
Si votre méthode appelle des méthodes publiques dans d'autres classes et que ces appels sont garantis par votre interface, vous pouvez alors vérifier que ces appels sont passés à l'aide d'un framework moqueur.
Vous ne devez pas utiliser la méthode elle-même (ni aucun de ses codes internes) pour générer le résultat attendu de manière dynamique. Le résultat attendu doit être codé en dur dans votre scénario de test afin qu'il ne change pas lorsque la mise en œuvre change. Voici un exemple simplifié de ce qu'un test unitaire devrait faire:
testAdd()
{
int x = 5;
int y = -2;
int expectedResult = 3;
Calculator calculator = new Calculator();
int actualResult = calculator.Add(x, y);
Assert.AreEqual(expectedResult, actualResult);
}
Notez que la manière dont le résultat est calculé n’est pas vérifiée - seulement que le résultat est correct. Continuez à ajouter de plus en plus de cas de test simples, comme ci-dessus, jusqu'à ce que vous ayez couvert autant de scénarios que possible. Utilisez votre outil de couverture de code pour voir si vous avez manqué des chemins intéressants.
Pour les tests unitaires, j'ai trouvé que Test Driven (tests first, code second) et code first, test second, étaient extrêmement utiles.
Au lieu d'écrire du code, écrivez un test. Écrivez le code puis regardez ce que vous pensez que le code devrait être en train de faire. Pensez à toutes les utilisations prévues et écrivez un test pour chacun. Je trouve que l'écriture de tests est plus rapide mais plus complexe que le codage lui-même. Les tests devraient tester l'intention. En réfléchissant également aux intentions, vous finissez par trouver des cas critiques lors de la phase d’écriture test. Et bien sûr, lors de la rédaction de tests, il est possible que l'un des rares usages provoque un bogue (quelque chose que je trouve souvent, et je suis très heureux que ce bogue n'ait pas corrompu les données et n'ait pas été vérifié).
Pourtant, le test revient presque à coder deux fois. En fait, j'avais des applications où il y avait plus de code de test (quantité) que de code d'application. Un exemple était une machine à états très complexe. Je devais m'assurer qu'après avoir ajouté plus de logique, tout fonctionnait toujours sur tous les cas d'utilisation précédents. Et vu que ces cas étaient assez difficiles à suivre en regardant le code, je me suis retrouvé avec une si bonne suite de tests pour cette machine que j'étais sûr qu'elle ne se casserait pas même après des modifications, et les tests m'ont sauvé quelques fois le cul . Et, alors que les utilisateurs ou les testeurs trouvaient des bugs sans suite dans le flux ou les angles, devinez quoi, ajoutés aux tests et ne se reproduisaient plus jamais. Cela a vraiment donné aux utilisateurs confiance dans mon travail en plus de rendre le tout super stable. Et quand il a fallu le réécrire pour des raisons de performances, devinez quoi, cela a fonctionné comme prévu sur toutes les entrées grâce aux tests.
Tous les exemples simples comme function square(number)
sont excellents et constituent probablement de mauvais candidats pour passer beaucoup de temps à tester. Ceux qui font la logique commerciale importante, c'est où les tests sont importants. Testez les exigences. Ne vous contentez pas de tester la plomberie. Si les exigences changent alors devinez quoi, les tests doivent aussi.
Les tests ne doivent pas être testés littéralement à trois reprises sur la fonction foo appelée. C'est faux. Vérifiez si le résultat et les effets secondaires sont corrects, pas la mécanique interne.
Il est intéressant de noter que l'adaptation ultérieure de tests unitaires au code existant est bien plus difficile que de conduire à la création de ce code avec des tests. C’est l’une des grandes questions liées au traitement des applications héritées ... comment effectuer des tests unitaires? Cela a déjà été demandé à plusieurs reprises auparavant (vous pouvez donc être fermé comme une question dupe), et les gens se retrouvent généralement ici:
Déplacement du code existant vers le développement piloté par les tests
J'appuie la recommandation de livre de réponse acceptée, mais au-delà de cela, il y a plus d'informations liées dans les réponses.
N'écrivez pas de tests pour obtenir une couverture complète de votre code. Écrire des tests qui garantissent vos exigences. Vous pouvez découvrir des chemins de code inutiles. Inversement, s’ils sont nécessaires, ils sont là pour remplir certaines exigences; trouvez-le et testez l'exigence (pas le chemin).
Gardez vos tests petits: un test par exigence.
Plus tard, lorsque vous devez apporter une modification (ou écrire un nouveau code), essayez d’abord d’écrire un test. Juste un. Ensuite, vous aurez franchi la première étape du développement piloté par les tests.
Le test unitaire concerne le résultat obtenu par une fonction/méthode/application. Peu importe la manière dont le résultat est obtenu, il importe simplement que ce soit correct. Par conséquent, votre approche consistant à compter les appels aux méthodes internes est erronée. Ce que j'ai tendance à faire est de rester assis et d'écrire ce qu'une méthode devrait renvoyer en fonction de certaines valeurs d'entrée ou d'un certain environnement, puis d'écrire un test comparant la valeur réelle renvoyée à ce que j'ai proposé.
Essayez d’écrire un test unitaire avant d’écrire la méthode à tester.
Cela vous obligera certainement à penser un peu différemment à la façon dont les choses se passent. Vous n'aurez aucune idée du fonctionnement de la méthode, mais de ce qu'elle est censée faire.
Vous devriez toujours tester les résultats de la méthode, et non comment la méthode obtient ces résultats.
les tests sont supposés améliorer la maintenabilité. Si vous modifiez une méthode et que le test rompt can peut être une bonne chose. D'un autre côté, si vous considérez votre méthode comme une boîte noire, son contenu ne devrait pas avoir d'importance. Le fait est que vous devez vous moquer des choses pour certains tests, et dans ces cas, vous ne pouvez vraiment pas traiter la méthode comme une boîte noire. La seule chose que vous puissiez faire est d'écrire un test d'intégration: vous chargez une instance totalement instanciée du service testé et vous la faites faire comme si elle fonctionnait dans votre application. Ensuite, vous pouvez le traiter comme une boîte noire.
When I'm writing tests for a method, I have the feeling of rewriting a second time what I
already wrote in the method itself.
My tests just seems so tightly bound to the method (testing all codepath, expecting some
inner methods to be called a number of times, with certain arguments), that it seems that
if I ever refactor the method, the tests will fail even if the final behavior of the
method did not change.
C'est parce que vous écrivez vos tests après avoir écrit votre code. Si vous le faisiez à l’inverse (vous avez d’abord écrit les tests), vous ne vous sentiriez pas de cette façon.