Définition et qualité des tests unitaires

Un test unitaire est un programme permettant de vérifier qu'une partie bien précise d'un logiciel fonctionne correctement

Les tests unitaires ont 3 intérêts :

  • Trouver les erreurs rapidement : le test étant réalisé sur un module particulier, le travail d'identification de la source de l'erreur est simple.
  • Documenter le code : si le test unitaire est suffisement complet, sa lecture nous renseigne sur la façon d'appeler une méthode
  • Sécuriser la maintenance et les évolutions : si lors d'une mise à jour, les tests unitaires indiquent des erreurs, nous sommes informés immédiatement des dysfonctionnements. On parle alors de test de non-régression

Les tests unitaires pourraient être vus comme des globules blancs, qui, en surnombre et bien entraînés, sont très redoutablement efficaces et pas chers contre les bugs de tous les jours. Ils ne suffisent toutefois pas car ces globules blancs sont entraînés pour n’éliminer que les comportements anormaux qu’on leur a appris à détecter.

les tests unitaires doivent suivre les principes FIRST

  • [F]ast, rapides, nous ne testons pas un grand volume de données mais des extraits uniquement
  • [I]solated, isolés, aucun test ne dépend d’un autre, pour qu’une collection de tests puisse être jouée dans n’importe quel ordre
  • [R]epeatable, répétables, joués N fois, produisent toujours le même résultat
  • [S]elf-validating, auto-validés, chaque test doit être capable de déterminer si son résultat est celui attendu ou non. Il doit déterminer s’il a réussi ou échoué. Il ne doit pas y avoir d’interprétation manuelle des résultats
  • [T]imely, opportuns, ils doivent être écrits à peu près en même temps que le code qu’ils testent

Types de tests

Il existe de très nombreux test qui sont réalisés lors du développement d'une application, et la typologie qui permet de les distinguer n'est pas universelle. Je vous propose une typologie basée sur le périmètre du test ainsi que sur le rôle du test :

Périmètre/RôleComportementIntégration
FonctionTest unitaireTest d'assemblage
ComposantTest fonctionnelTest d'intégration
De bout en bout/Test de bout en bout
Test unitaire

Permet de vérifier que chaque sous ensemble de l'application est testé de manière isolée

Test d'assemblage

Permet de vérifier que les sous-ensembles s'organisent bien entre eux.

Test fonctionnel

Permet de vérifier que le comportement fonctionnel obtenu est bien conforme avec celui attendu dans les spécifications

Test d'intégration

Permet de vérifier que les différentes fonctions s'organisent correctement

Test de bout en bout

Les tests de bout en bout testent une application ou un système d’un bout à l’autre et dans son ensemble de façon automatisée. Les tests réalisés lors du processus de test peuvent entre autres simuler des comportements de vrais usagers afin de valider les fonctionnalités d’une application.

En fonction du type de test, le coût de développement et le temps d'exécution sont différents :


Le Test Driven Developpement

Le test-driven development (TDD) renvoie à une technique de développement logiciel qui vise à réduire les anomalies d’une application en favorisant la mise en œuvre fréquente de tests.

  1. "Vous devez écrire un test qui échoue avant de pouvoir écrire le code de production correspondant"
  2. "Vous devez écrire une seule assertion à la fois, qui fait échouer le test ou qui échoue à la compilation"
  3. "Vous devez écrire le minimum de code de production pour que l'assertion du test en échec soit satisfaite".

Stratégie de test

Adapter le niveau de test signifie trouver la manière d'en faire le moins possible tout en maximisant leur efficacité et en tenant compte des contraintes du programme

Lorsqu'on gère une application, nous pouvons être confrontés à plusieurs problèmes avec nos tests unitaires :

  • Les tests unitaires n'existent pas : le problème le plus fréquent
  • Les tests unitaires échouent tous systématiquement : ce qui est équivalent au premier point
  • Les tests unitaires sont compliqués à maintenir : une évolution mineure du programme entraine une évolution majeure des tests unitaires
  • Les tests unitaires ne couvrent que très mal les fonctionnalités de l'application et le fait que les tests unitaires fonctionnent ne nous assure pas que le programme ne contient pas d'erreurs majeures

Comment donc se prémunir de ces problèmes lors du développement d'une application ? Voici quelques pistes qui peuvent vous aider à définir votre stratégie de test, d'autres solutions existent il s'agit d'une méthode personnelle :

Réaliser des tests simples

La maintenance du jeu de test sera couteuse, il convient donc de ne pas être trop ambitieux sur les tests, et de ne pas valider l'ensemble des résultats d'un traitement mais de ce concentrer sur l'essentiel

Par exemple, pour tester une requête qui insère 10 lignes dans une base de données, nous pouvons tester :

  • Que 10 lignes ont été insérées
  • Que les données de la première ligne sont correctes

Plutot que de tester le contenu du toutes les lignes ce test unitaire sera plus simple à maintenir et plus simple à écrire sans pour autant présenter plus de risques que le test complet

Se concentrer sur le scénario nominal

Un indicateur important pour les tests unitaires est la couverture de test : le nombre de lignes de code par lesquelles on passe pour effectuer un test. Cet indicateur peut nous pousser à écrire des tests unitaires balayant tous les cas possibles sans tenir compte des cas vraissemblables

Cette stratégie est une erreur : nous passerons beaucoup de temps à écrire des jeux de test pour valider des comportements qui n'arriveront jamais alors que les cas concrets seront sous représentés

Ainsi, il convient de ne tester les méthodes qu'avec des arguments vraissemblables : ne pas tester systématiquement les méthodes avec des paramètres null par exemple si ceci n'a pas de sens fonctionnel.

De cette manière, le jeu de test sera plus facile à maintenir et aura plus de chances d'identifier un bug important : si un bug à 1 chance sur 1000 de se produire et que sa gravité est minime alors le test unitaire ne sert à rien sur ce cas.

Penser à l'évolution des tests

Les tests doivent évoluer au même rythme que le code, il faut toujours écrire les tests unitaires dans l'objectif qu'ils soient toujours utilisés dans 10 ans. Ainsi il convient de mettre en place des méthodes permettant de nous assurer que le test fonctionnera encore dans plusieurs années

L'erreur que je vais décrire est très primaire mais néamoins fréquente : nous avons une classe GestionCalendrier qui permet de récupérer l'année courante, la méthode permettant de récupérer l'année fait appel à la date système

Nous mettons en place le test suivant :

@Test
public void testerGetAnnneCourante(){
	Assert.assertEquals(2017, GestionCalendrier.getAnneeCourante());
}

Dès l'an prochain, le code ne fonctionnera plus. Cette erreur peut également se cacher dans des requêtes sql faisant appel à CURENT_TIMESTAMP

Ne pas se baser sur un environnement extérieur aux programmes

Tôt ou tard, si nous utilisons des resources externes (bases de données) et que nous considérons que nous allons trouver la base dans un état donné, nous perdrons nos tests unitaires parce que la base ne sera plus à jour.

Pour éviter de perdre nos tests, il est important de réinitialiser les données de la base de données avant chaque test : nous ne savons pas a priori ce que la base contient