Introduction aux tests
Publié: 2021-06-16Bienvenue dans notre nouvelle série de blogs sur tout ce qui concerne les tests . Espérons que ces articles de blog vous donneront une idée générale de la façon dont nous les écrivons ici à Mediatoolkit et pourquoi. Avant de nous plonger dans des exemples, commençons par quelques définitions de base, des idées et des avantages de l'écriture de tests.
Quels types de tests existe-t-il ?
Tous les tests ne sont pas créés égaux. Il existe différents types de tests à des fins différentes. Bien que cet article se concentre principalement sur les tests unitaires , nous devons être conscients des différences et comprendre les avantages de chacun.
1. Essais manuels
Les tests manuels impliquent des tests exécutés manuellement par un testeur. Celles-ci sont généralement effectuées par l'assurance qualité ou par les développeurs eux-mêmes . L'assurance qualité doit être considérée comme la dernière ligne de défense, et non comme le principal testeur d'application (l'assurance qualité effectuant des tests manuels comme principal moyen de test de l'interface utilisateur est une exception valable à ce cas).
Les développeurs écrivent généralement des tests manuels lorsqu'ils veulent exécuter certaines choses localement et voir comment le système se comporte sans pousser le code vers la phase de mise en scène, ou Dieu vous protège, la production. Cependant, l'objectif de ces tests n'est pas la robustesse du code. Le seul rôle de ces tests est d' attraper les bugs et de s'assurer de la qualité de votre produit .
2. Essais d'intégration
Les systèmes sont composés de plusieurs composants, ou du moins ils devraient l'être. Les tests d'intégration vérifient l'API publique de ces composants. Pas seulement l'API REST, mais toute API exposée publiquement, telle que votre communication de sujet Kafka.
Si un composant "indique" qu'il génère un message sur un sujet dans un certain format, c'est son API publique ou son contrat. Les tests d'intégration vérifient si plusieurs composants développés individuellement peuvent communiquer et si leurs contrats correspondent en tant que groupe.
Si vous ne testez ces composants qu'individuellement, leur comportement individuel peut fonctionner correctement, mais lorsque vous essayez de les connecter dans un groupe plus important, vous découvrez que leurs contrats diffèrent .
3. Tests de bout en bout (E2E)
Les tests de bout en bout garantissent le flux d'expérience utilisateur du produit final . La complexité des systèmes d'aujourd'hui est difficile à couvrir avec des tests. De nombreux systèmes dépendent les uns des autres et les tests E2E garantissent que le produit fait ce qui est attendu . L'assurance qualité valide l'exactitude de votre produit en parcourant le flux des utilisateurs finaux et en vérifiant si tous les systèmes se comportent comme prévu.
4. Tests unitaires
Les tests unitaires sont l'épine dorsale de tout logiciel fiable . Ils constituent la base d'autres tests. Ils testent des unités individuelles.
Les développeurs peuvent confondre la définition d'une unité avec une seule méthode. Les tests unitaires doivent tester le comportement d'un composant qui peut être composé de plusieurs classes différentes. Les méthodes publiques accessibles par d'autres composants doivent être testées, ne testez pas les méthodes ou les classes protégées. Il s'agit de détails d'implémentation et non de votre API publique .
Pourquoi devrions-nous même tester?
Si nous écrivons de bons tests et les écrivons souvent, nous pouvons garantir la qualité de notre produit avant qu'il ne voie le jour.
À mesure que notre système vieillit, les avantages des tests deviennent de plus en plus apparents. Nos environnements deviennent fiables, ils économisent du temps de développement et beaucoup d'arrache-pied quand forcément les choses tournent mal. Vos collègues seront reconnaissants lorsqu'ils pourront jeter un coup d'œil à vos tests et comprendre ce que votre code doit et ne doit pas faire sans avoir à exécuter les choses manuellement .
Code robuste
Avant de parler de « code robuste », comment le définir ? Qu'est-ce qui rend le code robuste ? Cela signifie-t-il que nous devrions programmer de manière défensive et réfléchir à la manière dont d'autres développeurs pourraient abuser de notre code ?
Un code robuste est toujours simple et propre. La simplicité est robuste, la complexité est fragile . Nous devons gérer les entrées invalides, mais cela ne signifie pas que nous devons programmer de manière défensive et ne pas faire confiance à notre équipe.
Loi de Gall : Un système complexe qui fonctionne a invariablement évolué à partir d'un système simple qui a fonctionné.
La proposition inverse semble également être vraie : un système complexe conçu à partir de zéro ne fonctionne jamais et ne peut pas être fait fonctionner. Vous devez recommencer, en commençant par un système simple et fonctionnel.
Refactoriser en toute sécurité
Lorsque votre code est couvert par des tests, vous ne craignez plus de modifier le code existant . Après chaque changement, vous pouvez exécuter vos tests et vous assurer que vous n'avez rien cassé. Lorsque vous avez des tests, vous n'avez pas à programmer de manière défensive.
Le refactoring sans tests s'engage sur une pente glissante qui se terminera par des nuits blanches et des dimanches de travail. Ce sujet est trop vaste pour être traité ici et mérite un article de blog à lui seul à l'avenir.
Comment ne pas écrire de tests ?
Il est tout aussi important de savoir NE PAS écrire de tests unitaires que de savoir comment les écrire.
- L'écriture d'un test qui imprime le résultat d'un appel de méthode n'est pas un test puisque nous ne validons pas le résultat souhaité .
- Si votre test lit les données d'un fichier dans votre dossier Documents, il ne s'agit pas d'un véritable test unitaire puisque les tests ne doivent pas dépendre de l'environnement.
- Tout développeur devrait pouvoir vérifier votre code et exécuter les tests avec succès sans rien faire d'autre.
- Chaque test unitaire doit être indépendant des autres tests . Cela implique que l'ordre d'exécution de vos tests ne devrait pas non plus avoir d'importance.
- L'exécution de tests plusieurs fois devrait toujours se terminer par les mêmes résultats si nous ne modifions aucun code.
- Les tests unitaires doivent tester des comportements , et non des appels de méthode individuels. Toutes les classes et méthodes n'ont pas besoin d'avoir leur propre test.
Le comportement est quelque chose qui produit une valeur réelle dont l'utilisateur de votre système a besoin. Votre utilisateur a-t-il besoin de savoir si productFactory.create() créé le même objet lorsqu'il a été appelé deux fois ou si votre référentiel a été appelé avec certains paramètres ? Probablement pas, mais de nombreux développeurs écrivent exactement ce type de tests.
Si vos tests ressemblent à cela, ils sont étroitement liés à votre implémentation. Chaque fois que vous souhaitez modifier les détails de votre implémentation, vous devez mettre à jour vos tests, même si le comportement est le même. Vos tests ne doivent changer que lorsque le comportement change, pas les détails d'implémentation . En d'autres termes, testez ce que fait votre code, pas comment il le fait.
Comment écrit-on les tests ?
Nos tests doivent suivre les meilleures pratiques de code , ils doivent être indépendants de l'environnement et ils doivent s'exécuter rapidement .
Il est important de garder le temps d'exécution des tests aussi court que possible. Chaque test ne devrait pas prendre plus de quelques millisecondes . Lorsque les tests prennent trop de temps à s'exécuter, les gens ont tendance à les ignorer et à se fier uniquement à leur serveur CI, tel que Jenkins, pour faire des histoires lorsqu'il ne peut pas créer leurs exécutables de déploiement.

Chaque test est composé de 3 sections 'A' (le modèle AAA) :
- Organiser
- Acte
- Affirmer
1. Organiser
Dans la section organiser de notre test, nous nous assurons que notre système est dans un état spécifique avant d'appeler le comportement que nous voulons tester . Le « système » peut être un objet que nous devons configurer de manière spécifique pour produire un comportement, créer des fichiers temporaires ou des choses de cette nature.
Le code de cette section est généralement plus grand que les deux autres combinés .
Un modèle de conception qui devrait s'avérer particulièrement utile pour garder cette section petite est Object Mother . Ce modèle de conception est très similaire à Factory , mais il a des méthodes plus spécifiques qui créent des objets préconfigurés pour vous. Alors qu'une Factory standard pourrait avoir une méthode telle que createCar(carDescription) , un ObjectMother aura des méthodes comme createRedFerrari() , createBlackTesla() ou createBrokenYugo() .
2. Agir
Cette section de votre test doit comporter une ligne . Cette ligne exécute le comportement testé. Si vous vous retrouvez à écrire plus d'une ligne pour cette section, vous n'avez probablement pas la bonne encapsulation de votre comportement . Vos clients ne devraient pas s'attendre à appeler plusieurs méthodes de votre objet dans un ordre particulier, alors pourquoi vos tests le feraient-ils ?
Cette ligne est un appel de méthode que nous voulons tester. Si cette méthode renvoie un résultat, vous devez stocker cette valeur dans une variable pour vérifier s'il s'agit de la valeur attendue à l'étape Assert.
3. Affirmez
Après avoir préparé le système dans la section Arrange et exécuté notre action sous test dans la section Act, nous devons valider le résultat de l'action. Nous vérifions généralement le résultat de la méthode ici, mais parfois, nos méthodes ne renvoient pas de valeurs, mais elles produisent toujours des effets secondaires. Si notre code devait changer l'état d'un objet, créer un fichier ou supprimer quelque chose d'un List , nous devrions vérifier s'il a fait exactement cela.
Stubs vs Mocks
La plupart des développeurs utilisent les termes mock et stub de manière interchangeable, mais il existe des différences.
Un talon ne peut pas échouer au test, un faux peut.
Les stubs sont basés sur l'état , ils renvoient des valeurs codées en dur. « Quel résultat ai-je obtenu ? »
Les simulations sont basées sur le comportement , vous les utilisez pour vérifier comment votre comportement le traverse. « Comment ai-je obtenu le résultat ? »
Si vos tests unitaires sont jonchés de simulacres, vos tests finissent par être très fragiles, ce qui signifie qu'à chaque fois que vous modifiez l'un de vos détails d'implémentation, vous devez mettre à jour tous vos appels fictifs.
Testez une seule chose.
Nous devons être capables d' isoler un comportement et de prouver qu'il fonctionne . Si ce comportement doit fonctionner différemment avec différentes entrées, nous devons écrire un nouveau test pour chacun de ces comportements. Il est difficile de savoir pourquoi notre test a échoué si nous avons un grand test qui teste plusieurs choses à la fois. De plus, il devient plus difficile de supprimer les fonctionnalités dont nous n'avons plus besoin et de voir quelles fonctionnalités nous avons cassées lorsque nous ajoutons du nouveau code.
Il est parfaitement acceptable d'avoir plusieurs assertions dans la dernière section de vos tests tant que cela ne nécessite pas que vous appeliez le comportement testé plusieurs fois. Si tel est le cas, il sera difficile d'identifier le comportement défectueux et de le corriger.
Lorsque nous affirmons plusieurs comportements dans un test, nous n'obtenons pas une image claire de ce qui ne fonctionne pas exactement, car le test ne signalera que le premier échec et les autres seront ignorés. Il devient beaucoup plus difficile de comprendre quels changements sont nécessaires et combien de choses ne fonctionnent pas comme prévu.
Essais de nommage
Une chose qui sépare les bons tests des bons tests est le nom du test. Les tests ne doivent pas seulement nous dire ce qu'ils font, mais quand ils le font.
Il existe de nombreux bons modèles de dénomination que vous pouvez utiliser, alors choisissez celui que vous trouvez le plus descriptif et respectez-le. Voici quelques exemples de grands noms de test :
- RegistrationServiceShould.createNewAccountWhenEmailIsNotTaken
- RegistrationServiceTest.whenEmailIsFree_createNewAccount
- RegistrationServiceTest.if_freeEmail_when_userCreatesAccount_then_create
Lorsque vous écrivez des tests unitaires, il est beaucoup plus important de communiquer ce que vous testez que de suivre les meilleures pratiques de dénomination des méthodes. Par exemple, en Java, nous utilisons camelCase lors de l'écriture de méthodes, mais il est parfaitement valide d'utiliser le trait de soulignement (_) pour séparer l'état de l'action dans votre nom de test.
Essais propres
Les tests que vous écrivez doivent suivre toutes les pratiques de code propre que vous appliquez à votre code. Les tests ne sont pas des citoyens de seconde classe et vous devez appliquer le même niveau de soin que vous faites avec le reste de votre code pour les rendre lisibles.
La définition de la duplication de code dans les tests est très importante. Le principe DRY (Don't Repeat Yourself) s'applique à l'extraction d'un comportement qui change pour la même raison. Les tests changent pour différentes raisons, alors soyez varié dans l'extraction des éléments de vos tests s'ils ne changent vraiment pas pour la même raison. Alerte spoiler, ils ne le font souvent pas.
if les instructions n'appartiennent pas aux tests. L'instruction if nous dit que notre test fait au moins deux choses différentes et nous serions mieux si nous réécrivions notre test en deux tests différents. Lorsqu'ils sont nommés correctement, il sera plus facile de comprendre ce que font les tests et quels sont tous les différents comportements.
Quand doit-on écrire des tests ?
Guidés par les principes de TDD, nous devrions écrire des tests avant d'écrire un nouveau code .
Lorsque nous devons ajouter une nouvelle fonctionnalité, nous décrivons d'abord le comportement souhaité comme un nouveau test. Nous apportons le moins de modifications nécessaires pour réussir ce test sans en casser d'autres.
Sinon, plus le temps passe, plus nous avons de code qui n'a pas été testé et les risques d'introduction de bogues ou de suringénierie augmentent .
De plus, les tests deviennent plus complexes car nous avons plus de code à tester maintenant, et ce qui se passe généralement, c'est que les développeurs ajustent les tests au code. Cela signifie que nous ajustons le comportement pour qu'il corresponde à ce que fait notre code au lieu de l'inverse.
Lorsque nous écrivons des tests tôt, la définition du problème devient plus petite et il est plus facile de comprendre ces problèmes que des comportements plus génériques et complexes.
Dernières pensées
Bien que la rédaction de tests puisse sembler une chose facultative à faire, il est crucial de commencer avec de bonnes bases. Le codage est déjà difficile, facilitez-le pour vous et vos coéquipiers en écrivant un code testable qui est plus facile à lire, à comprendre et à maintenir . Enfin, si vous avez des difficultés à rendre vos tests conformes à vos souhaits, le problème se situe plus probablement dans votre code que dans vos tests.
Intéressé à travailler sur un code propre et testable ?
Découvrez notre offre d'emploi pour Développeur Backend Senior
ou envoyez-nous une candidature spontanée !
