
Tests et amélioration du code
Découvrez comment les tests et l'amélioration continue du code (refactoring) sont liés dans un cycle itératif de développement logiciel. Apprenez à utiliser les tests pour guider l'amélioration de votre code Python, et à refactoriser en toute confiance.
Un cycle itératif : tests, code, refactoring
Le développement logiciel est rarement un processus linéaire. Il est plus souvent un cycle itératif, où l'on alterne entre l'écriture de code, l'écriture de tests, l'exécution des tests, et l'amélioration du code (refactoring).
Ce cycle peut être visualisé comme suit :
- Ecrire un test : Avant d'ajouter une nouvelle fonctionnalité ou de modifier du code existant, écrivez un test unitaire qui vérifie le comportement attendu.
- Exécuter les tests : Exécutez tous les tests, y compris le nouveau test. Le nouveau test devrait échouer (puisque le code correspondant n'a pas encore été écrit).
- Ecrire le code : Ecrivez le code nécessaire pour que le nouveau test passe.
- Exécuter les tests : Exécutez à nouveau tous les tests. Tous les tests devraient maintenant passer.
- Refactoriser (améliorer) le code : Une fois que les tests passent, vous pouvez améliorer la structure interne du code (le rendre plus clair, plus concis, plus efficace) sans changer son comportement externe. Les tests vous garantissent que vous n'introduisez pas de régressions (de nouveaux bugs) pendant le refactoring.
- Répéter : Recommencez le cycle pour ajouter de nouvelles fonctionnalités, corriger des bugs, ou améliorer le code existant.
Cette approche itérative, où les tests guident le développement, est au coeur de méthodes comme le Test-Driven Development (TDD).
Les tests comme filet de sécurité
Les tests unitaires agissent comme un *filet de sécurité* lors du développement et de la maintenance du code.
Lorsque vous modifiez du code, vous pouvez exécuter les tests pour vous assurer que vous n'avez pas cassé quelque chose qui fonctionnait auparavant. Si un test échoue après une modification, cela vous indique immédiatement qu'il y a un problème.
Sans tests, il est beaucoup plus difficile de modifier du code en toute confiance, surtout dans les grands projets ou les projets avec beaucoup de code hérité.
Les tests vous permettent de :
- Détecter les régressions : Un bug qui a été corrigé peut réapparaître si le code est modifié. Les tests permettent de détecter ces régressions immédiatement.
- Refactoriser en toute confiance : Vous pouvez améliorer la structure interne du code sans craindre de casser le comportement existant.
- Ajouter de nouvelles fonctionnalités plus sereinement : Vous pouvez ajouter de nouvelles fonctionnalités en sachant que vous ne risquez pas de perturber le code existant.
Exemple : cycle de développement avec tests
Imaginons que nous voulions ajouter une fonction `diviser` à notre module, qui gère correctement la division par zéro.
Voici un exemple de cycle de développement, en utilisant des tests :
- Ecrire un test (qui échoue initialement) :
# test_mon_module.py import unittest from mon_module import diviser class TestDivision(unittest.TestCase): def test_division_par_zero(self): with self.assertRaises(ZeroDivisionError): diviser(10, 0)Ce test vérifie que la fonction `diviser` lève bien une `ZeroDivisionError` lorsqu'on essaie de diviser par zéro. Au début, ce test échouera (car la fonction `diviser` n'existe pas encore, ou ne gère pas le cas de la division par zéro).
- Ecrire le code minimal pour que le test passe :
# mon_module.py def diviser(a, b): if b==0: raise ZeroDivisionError("Division par zéro") return a / bOn écrit le code minimal pour que le test passe (ici, lever une `ZeroDivisionError` si `b` est zéro).
- Exécuter les tests : On exécute les tests (par exemple, avec `python -m unittest test_mon_module.py`). Le test devrait maintenant passer.
- Ajouter d'autres tests (et le code correspondant) : On peut ajouter d'autres tests pour vérifier le comportement de la fonction dans d'autres cas (par exemple, division de nombres positifs, de nombres négatifs, etc.).
- Refactoriser (si nécessaire) : Une fois que tous les tests passent, on peut améliorer la structure interne du code (par exemple, simplifier la fonction `diviser`), en s'assurant que les tests continuent de passer.
Ce cycle itératif permet de développer du code de manière incrémentale, en s'assurant à chaque étape que le code fonctionne comme prévu.
Amélioration continue du code (refactoring)
Le refactoring est le processus d'amélioration de la structure interne du code *sans changer son comportement externe*. L'objectif du refactoring est de rendre le code plus clair, plus lisible, plus maintenable, et plus efficace.
Les tests unitaires sont *essentiels* pour le refactoring. Ils vous permettent de vous assurer que vous ne modifiez pas le comportement du code lorsque vous le refactorisez.
Quelques exemples de refactoring :
- Renommer des variables ou des fonctions pour les rendre plus descriptives.
- Extraire des portions de code répétitives dans des fonctions.
- Simplifier des expressions complexes.
- Réorganiser le code pour le rendre plus logique.
- Supprimer du code mort (du code qui n'est jamais exécuté).
- Améliorer les commentaires et les docstrings.
- Déplacer du code vers d'autres modules ou packages.
Après chaque étape de refactoring, exécutez les tests pour vérifier que vous n'avez pas introduit de régressions.
Le refactoring est un processus continu. Il ne s'agit pas de réécrire complètement le code, mais d'apporter de petites améliorations incrémentales pour maintenir la qualité du code au fil du temps.
Couverture de code (code coverage)
La couverture de code (code coverage) est une mesure qui indique quelle proportion de votre code est exécutée par vos tests.
Un outil de couverture de code (comme `coverage.py` ou le plugin `pytest-cov` pour `pytest`) peut vous dire quelles lignes de votre code ont été exécutées par les tests, et quelles lignes n'ont pas été exécutées.
Une couverture de code élevée (proche de 100%) est un bon indicateur de la qualité de vos tests, mais ce n'est pas une garantie absolue. Il est possible d'avoir une couverture de 100% et d'avoir quand même des bugs (si les tests ne vérifient pas les bonnes conditions, ou s'ils ne couvrent pas tous les cas d'utilisation possibles).
La couverture de code est un outil utile, mais il ne faut pas la considérer comme une fin en soi. L'objectif principal est d'avoir des tests *pertinents* qui vérifient le comportement de votre code.
Exemple d'utilisation de `coverage.py` :
pip install coverage
coverage run -m unittest # Exécute les tests et mesure la couverture
coverage report # Affiche un rapport de couverture sur la console
coverage html # Génère un rapport de couverture au format HTML`coverage.py` peut être intégré avec `pytest` et d'autres outils.