Skip to content

Tests unitaires avec nosetests

Alexandre Gingras-Courchesne edited this page May 10, 2016 · 1 revision

Le projet emploi la suite de tests unitaires nosetests. Celle-ci est disponible avec Anaconda ou par installation à l'aide de pip.

e.g: sudo pip install nose

Pourquoi tester?

Pour un projet avec de multiples collaborateurs et de longue haleine, il y a plusieurs raisons:

  • Faciliter les cycles de refactor
  • Documenter l'utilisation du code
  • Éviter les régressions lors de l'ajout ou de la modification de code
  • Solidifier la base commune du code

Les refactor sont des opérations fréquentes et très utiles pour maintenir du code sur une longue durée. Cependant, il est probable que l'auteur original du code se soit désengagé du projet lorsqu'un refactor est effectué, ou qu'il ne se souvienne tout simplement plus des détails concernant ce code. Avec une suite de tests unitaires, il est facile de valider si le code produit les mêmes résultats après le refactor.

En écrivant les tests unitaires, les différentes façons d'utiliser les méthodes et les classes sont documentées. Les séries d'appels logiques que les auteurs avaient en tête sont démontrées, ainsi que des valeurs d'arguments considérées normales ou critiques.

Lorsqu'un bogue est découvert, écrire un test unitaire oblige le programmeur à comprendre ce qui le cause et permet ensuite de tester automatiquement ce cas particulier. Par la suite, si une modification réintroduit le bogue, il est immédiatement détecté.

Enfin, une suite de tests unitaires augmente la confiance envers le code source. Les résultats et comportements de base des différentes fonctions sont validés et prouvés être fonctionnel. Si les tests sont bien écrits, les fonctions sont testées individuellement et en isolation. Les conditions externes sont contrôlées et des cas extrêmes peuvent être utilisés pour vérifier que la fonction continu à retourner des résultats adéquats.

Caractéristiques essentiels d'un test unitaire

Quelques caractéristiques sont indispensables à un test unitaire.

  • Atomique
  • Exhaustif
  • Rapide
  • Reproductible
  • Échoue sans code

Un test devrait toujours tester un seul élément en isolation. Une seule fonction et si la fonction est complexe; une seule propriété de cette fonction. Les cas d'entrées et de sorties devraient être exhaustif. Il faut chercher à prouver que la fonction est capable de prendre en entrée toutes les classes possibles d’arguments et que toutes ses sorties potentielles sont cohérentes. Ceci inclut entre autre de tester si la fonction retourne un exception lorsqu'elle le devrait.

Une caractéristique extrêmement importante est que le test doit être rapide. L'ensemble de la suite de tests concernant un module doit pouvoir s'exécuter en moins d'une seconde. Autrement lancer les tests devient plutôt une corvée. Les tests unitaires sont un outil qui devraient pouvoir rouler en continu lors de l'écriture du code et le programmeur ne devrait jamais attendre après l'exécution des tests pour pouvoir poursuivre son travail.

Ne pas être capable de reproduire le résultat des tests peu importe l'environnement externe -- e.g: Linux vs Windows ou la version du simulateur, mine la confiance envers les tests. Trouvent-ils vraiment des bogues ou des subtilités dans la configuration?

Enfin, un test ne devrait jamais passer sans aucune ligne de code. C'est souvent une indication que le test est mal écrit et ne prouve pas réellement que le comportement de la fonction est adéquat.

Isoler le code

Parfois il est difficile de bien tester un module ou une partie du code source. Idéalement, un maximum du code source devrait être des fonctions avec des entrées et des sorties bien définies. Des fonctions se testent très facilement avec des données artificielles et en vérifiant que les sorties correspondent à ce qui est attendu.

Du code qui agit par effets de bords (side effect) est beaucoup plus difficile à bien tester. Il faut à ce moment l'isoler et vérifier les valeurs affectées. Dans un tel cas, envisager un refactor pour rendre le code fonctionnel est une bonne idée. Il est préférable de concentrer les effets de bords à quelques endroits et d'obtenir les valeurs nécessaires à l'aide de plusieurs fonctions différentes. À ce moment, les fonctions peuvent être validés et la méthode ou le script qui s'occupe de modifier l'environnement peut-être étroitement testés.

Il s'agit un peu d'un paradoxe, une des motivations d'écrire de bon tests unitaires est de pouvoir effectuer un refactor plus facilement et en confiance. L'approche recommandé est d'y aller itérativement. Modifier une section précise du code, créer des petites fonctions utilitaires et commencer à les tester.

Simuler l'environnement

Parfois, un module nécessite tout simplement d'accéder à plusieurs ressources. Il alors possible d'utiliser des mocks, des objets qui imites le comportement d'une ressource complexe ou d'un module complet. L'objectif du test n'est pas de prouver que ce module externe, il possède déjà ses tests unitaires, ou cette ressource est valide. TODO: Trouver quel support Python offre pour les mocks

Intégration continue

Pour supporter d'avantage les processus de travaux, une intégration continue est en place (pas en date du 10 mai 2016). N'importe quel membre de l'équipe peut faire un appel à l'intégration continue contre un de ces forks et consulter les résultats. De plus, l'intégration permet aussi de lancer les tests lors du merge d'un Pull Request, ce qui aide les membres de l'équipe de code review à faire leur travail et à mieux évaluer les modifications envoyées.

Exemple de tests unitaires

TODO: Une fois quelques suites bien écrites, les référer et décortiquer un ou deux exemples pointus (e.g: des éléments dans STA ou un module qui parle au simulateur)