Maitriser Unittest - Partie 1
Mise à jour :
Si vous avez suivi mon précédent guide sur les tests Python, vous savez déjà que le TDD consiste à écrire vos tests avant même d’écrire une ligne de code. Avec le TDD, chaque fonctionnalité est soigneusement définie, testée et optimisée en suivant le cycle Red-Green-Refactor. Mais pour mettre en pratique cette approche, il faut un outil capable de structurer ces tests. C’est là que unittest entre en jeu.
unittest, la bibliothèque standard de Python pour les tests unitaires, est l’outil parfait pour appliquer le TDD. Pourquoi ? Parce qu’il est robuste, bien documenté, et surtout intégré directement dans Python. Vous n’avez pas besoin d’installer de librairie supplémentaire : tout est prêt à l’emploi.
Codez votre premier test
Avant de plonger dans l’utilisation de unittest, la bonne nouvelle, c’est qu’il n’y a rien à installer ! En effet, unittest fait partie intégrante de la bibliothèque standard de Python. Tout ce dont vous avez besoin, c’est une installation fonctionnelle de Python. Maintenant, voyons comment organiser votre environnement pour bien démarrer.
Organiser vos fichiers et dossiers pour les tests
Une bonne organisation de vos fichiers est essentielle pour un projet maintenable. Voici une structure classique que j’aime utiliser :
Répertoiremon_projet/
Répertoiresrc/ # Contient le code source
- exemple.py # Exemple de fichier de code
Répertoiretests/ # Contient vos tests unitaires
- test_exemple.py # Tests pour exemple.py
- init .py # Indique que c’est un package Python
- requirements.txt # Optionnel, pour gérer les dépendances
Avec cette structure :
- Le dossier
src/
contient votre code principal. - Le dossier
tests/
est dédié à vos fichiers de tests. Chaque fichier de test correspond idéalement à un fichier dans le dossiersrc/
.
Configurer votre environnement pour unittest
Créez un fichier de test pour vous assurer que tout fonctionne correctement.
Voici un exemple minimaliste dans tests/test_exemple.py
:
Ensuite, lancez le fichier avec cette commande :
Si tout est bien configuré, vous verrez une sortie similaire à celle-ci :
Les bases de unittest : assertions et structure
Maintenant que votre environnement est prêt, il est temps de plonger dans les bases de unittest. Dans cette partie, nous allons découvrir les assertions, qui sont au cœur des tests, et la manière de structurer vos fichiers pour que tout reste clair et efficace.
Comprendre les assertions
Les assertions sont des commandes utilisées pour vérifier que le résultat attendu correspond bien au résultat obtenu. Si une assertion échoue, le test est marqué comme échoué. Voici quelques assertions courantes que vous utiliserez souvent :
-
assertEqual(a, b) : Vérifie que
a
est égal àb
. Exemple : -
assertNotEqual(a, b) : Vérifie que
a
n’est pas égal àb
. -
assertTrue(expr) : Vérifie que
expr
est vrai. Exemple : -
assertFalse(expr) : Vérifie que
expr
est faux. -
assertIs(a, b) : Vérifie que
a
etb
sont le même objet (par identité). -
assertIsNone(expr) : Vérifie que
expr
estNone
. -
assertRaises(exception, callable, *args, **kwargs) : Vérifie qu’une exception spécifique est levée lors de l’appel d’une fonction ou méthode.
Avec ces assertions, vous pouvez couvrir presque tous les cas possibles dans vos tests.
Appliquer le cycle TDD avec unittest
Le Test-Driven Development (TDD) est une approche méthodique où les tests guident le développement de votre code. Avec unittest, ce processus devient fluide et intuitif. Dans ce chapitre, je vais vous montrer comment suivre le cycle Red-Green-Refactor avec des exemples concrets.
Le cycle TDD : Red-Green-Refactor
-
Red : Écrire un test qui échoue Vous commencez par écrire un test pour une fonctionnalité que vous souhaitez implémenter. Comme le code n’existe pas encore (ou est incomplet), ce test échouera. Cet échec est essentiel pour vérifier que le test détecte bien les problèmes.
-
Green : Écrire juste assez de code pour réussir le test Ensuite, vous implémentez le minimum de code nécessaire pour que le test passe. L’idée ici n’est pas d’écrire du code parfait, mais de prouver que la fonctionnalité de base fonctionne.
-
Refactor : Améliorer le code sans casser le test Une fois que le test passe, vous pouvez améliorer votre code : le rendre plus lisible, optimiser les performances, ou éliminer les redondances. Les tests déjà écrits garantissent que votre refactorisation n’introduit pas de bugs.
Exemple pratique : Une fonction de calcul d’intérêts
Prenons un cas simple : une fonction qui calcule des intérêts simples. Voici comment appliquer le TDD.
Étape 1 : Red - Écrire un test qui échoue
Commençons par écrire un test pour une fonction appelée calculer_interets
.
Cette fonction n’existe pas encore, mais nous définissons ce qu’elle est censée
faire :
Créer le fichier du code en ne déclarant que la fonction :
En exécutant ce test, vous obtiendrez une erreur du type :
Étape 2 : Green - Implémenter juste assez de code
Créons maintenant une version minimale de la fonction pour faire passer le test :
En relançant le test, voici le résultat :
Le test passe. Mission accomplie pour cette étape !
Étape 3 : Refactor - Améliorer le code
Regardons s’il y a des améliorations possibles. Par exemple, nous pourrions ajouter une validation pour les entrées négatives ou des taux supérieurs à 1 (ce qui serait irréaliste pour un taux d’intérêt annuel).
Ajoutons un nouveau test pour vérifier ce comportement :
En réexécutant tous les tests, vous validez que la refactorisation n’a pas cassé les fonctionnalités existantes.
Codons notre fonction pour qu’elle lève le raise attendu :
On relance les tests :
Yes
Les bénéfices du cycle TDD avec unittest
- Développement guidé par les fonctionnalités : Vous écrivez du code en réponse à des besoins précis, ce qui évite de coder des fonctionnalités inutiles.
- Moins de bugs : Les tests vous alertent immédiatement si une modification casse une fonctionnalité existante.
- Code maintenable : Le processus de refactorisation garantit que votre code reste propre et facile à comprendre.
Un conseil pour bien démarrer
Au début, il peut être tentant d’écrire une grande partie du code avant les tests. Résistez à cette tentation ! Respecter le cycle Red-Green-Refactor vous aidera à développer un code plus robuste, tout en vous habituant à penser aux tests comme une partie intégrante du développement.
Éviter les pièges courants avec unittest
Même si unittest est un outil puissant, il est facile de tomber dans des pièges qui rendent vos tests moins efficaces, voire inutiles. Dans ce chapitre, nous allons examiner les erreurs courantes et les meilleures pratiques pour les éviter.
Écrire des tests trop dépendants du code
Le piège : Vos tests sont si étroitement liés à l’implémentation actuelle qu’une petite modification (comme un changement de nom de méthode ou de structure) casse de nombreux tests. Cela rend leur maintenance difficile et décourage l’écriture de tests.
Comment l’éviter :
- Testez le comportement, pas l’implémentation. Concentrez-vous sur ce que le code doit faire, et non sur comment il le fait.
- Si possible, utilisez des interfaces stables ou des fonctions publiques comme points d’entrée pour vos tests.
Exemple mauvais :
Exemple correct :
Ne pas tester les cas limites
Le piège : Vous testez uniquement les scénarios “heureux”, c’est-à-dire les cas où tout fonctionne parfaitement, en oubliant les situations inhabituelles ou les erreurs possibles.
Comment l’éviter :
- Ajoutez des tests pour les entrées non valides, comme des valeurs nulles, des nombres négatifs, ou des chaînes vides.
- Simulez des erreurs système, comme une base de données inaccessible ou un fichier introuvable.
Exemples à tester :
- Que se passe-t-il si on passe
None
à votre fonction ? - Votre fonction retourne-t-elle une exception appropriée pour un taux d’intérêt négatif ?
Ignorer la lisibilité des tests
Le piège : Vos tests sont complexes, avec des noms de méthodes peu explicites ou des blocs de code durs à lire. Les tests deviennent aussi difficiles à comprendre que le code qu’ils testent.
Comment l’éviter :
- Donnez des noms explicites à vos tests pour indiquer clairement leur objectif.
- Utilisez des commentaires ou des docstrings pour expliquer des scénarios complexes.
Exemple mauvais :
Exemple correct :
Tester plusieurs choses dans un seul test
Le piège : Vous combinez plusieurs vérifications dans un seul test. Si une vérification échoue, il est difficile de savoir exactement où se situe le problème.
Comment l’éviter :
- Chaque test doit avoir un objectif unique. Divisez les tests si nécessaire.
- Utilisez des classes ou des fichiers séparés pour tester des fonctionnalités distinctes.
Exemple mauvais :
Exemple correct :
Ne pas exécuter les tests régulièrement
Le piège : Vous écrivez des tests, mais vous les exécutez rarement, ou uniquement avant une mise en production. Cela augmente le risque de bugs.
Comment l’éviter :
- Exécutez les tests à chaque modification majeure du code.
- Intégrez les tests dans un pipeline CI/CD pour les exécuter automatiquement à chaque commit.
Exemple : Avec Gitlab CI,
configurez un fichier .gitlab-ci.yml
pour automatiser vos tests :
Conclusion
Félicitations ! Vous avez maintenant une solide compréhension des bases de unittest : de l’écriture de tests simples, à leur structuration en suites bien organisées, tout en évitant les pièges courants. Vous avez également appris à exécuter et regrouper vos tests pour garantir un workflow fluide et maintenable.
Mais le chemin ne s’arrête pas là. Les projets réels sont rarement isolés et impliquent souvent des dépendances externes : bases de données, API, ou autres services tiers. Tester ces interactions peut vite devenir compliqué, mais heureusement, nous avons des outils comme les mocks et les stubs pour simuler ces dépendances et garder nos tests rapides et fiables.
Dans la seconde partie de ce guide, nous plongerons dans l’univers du mocking avec unittest.mock. Vous apprendrez à isoler vos tests, simuler des comportements complexes, et valider les interactions entre vos modules et leurs dépendances.
Alors, prêt à pousser vos compétences en tests encore plus loin ? 🚀 On se retrouve dans la prochaine partie !