Quand et où écrire des tests unitaires ?

Cette page importante vous permettra de vous familiariser avec la notion de couverture de test, et vous donnera quelques indications sur le quoi, le où et le quand tester. À assimiler avant de lire la suite !

La notion de couverture de test

Quand on commence à implémenter des tests unitaires dans un système informatique, on en vient très vite à se poser une question non-triviale : on s'arrête où, au juste ?

C'est alors le bon moment pour introduire la fameuse notion de couverture de test.

Définition La couverture de test (parfois appelée couverture de code, code coverage en anglais) est la mesure du taux de code source testé dans un système informatique.

Des outils de couverture de code

Il existe des outils automatiques, souvent liés aux outils de test (test runners notamment), qui permettent de fournir au programmeur une vision de la couverture de code. Un exemple d'outil de check de couverture de code est coverage.py, qui permet d'établir la couverture de code d'un programme Python.

Voici un exemple de rapport généré par un tel outil :

Programme à tester
def say_hello():
    """
    Cette fonction dit bonjour !
    """
    print('Hello')

def is_even(nbr):
    """
    Cette fonction teste si un nombre est pair.
    """
    return nbr % 2 == 0
Code de test
import unittest

class MyTest(unittest.TestCase):
    def test_is_even(self):
        self.assertTrue(is_even(2))
        self.assertFalse(is_even(1))
        self.assertEqual(is_even(0), True)

if __name__ == '__main__':
    unittest.main()
Résultat
.
Name        Stmts   Miss  Cover   Missing
-----------------------------------------
programme       5      1    80%   4

Le résultat est facilement compréhensible : notre test s'est attardé sur la fonction test_is_even(), mais aucun test n'a été réalisé sur la fonction say_hello() : la ligne implémentée dans say_hello() n'est jamais couverte par les tests, d'où un « miss ». Nous avons ici une couverture de test de 80%.

La quête de la couverture de test optimale…

Faut-il tester 80% du code source d'un logiciel, 10% ou une autre proportion ? Cette question est en fait une question difficile, presque inutile.

Important Commençons par bien comprendre la problématique : aboutir à une couverture de code de 100% signifierait que chaque instruction du programme serait exécutée au moins une fois par les tests déployés.

La couverture fonctionnelle

Au delà de la couverture de code qui est un indicateur parmi d'autres, il est important de se concentrer sur la couverture fonctionnelle procurée par les tests mis en œuvre. Est-ce que les test unitaires développés permettent de vérifier 80% des fonctionnalités du système, ou alors de vérifier de manière très fine 5% du système ?

Une couverture fonctionnelle forte est généralement un gage de réussite en matière de testing : testez tout. Pas forcément dans les moindres recoins, mais testez tous vos modules.

Remarques sur la rentabilité

Certaines brutes vous martèleront qu'il faut absolument tester 100% de votre code, systématiser et automatiser l'utilisation des outils de code coverage, etc. Bien entendu, à moins de travailler sur un projet dont la taille est complètement démesurée, c'est parfois possible… mais à quel prix ?

N'oublions pas que le fait d'écrire du code de test, mais également de le maintenir, a un coût. Pour que la manœuvre soit rentable, il faut que les bénéfices que l'on en tire soient supérieurs à ce coût : élémentaire n'est-ce pas ?

Vous devez donc vous poser la question du seuil de rentabilité de vos tests : quels sont les modules critiques du système, quels sont les modules moins important, ou encore ceux pour lesquels des tests unitaires ne sont peut-être pas la meilleure solution pour s'assurer d'une qualité optimale. Parfois, le mieux est l'ennemi du bien…

Que tester ?

Ok, nous avons vu qu'il n'était pas nécessairement utile ni possible de tester unitairement l'intégralité du code d'une application. Il est donc de rigueur de cibler adroitement ses efforts pour écrire de bons tests unitaires pour les bons composants.

Les bons candidats pour des tests unitaires

Les meilleurs candidats pour les tests unitaires sont généralement les composants qui agissent comme une « boite noire » : ces composants prennent des données en entrée et fournissent des données en résultat (devant être conformes à une spécification), tout en ayant réalisé à cet effet un ensemble de traitement, sorte de tambouille interne dont le consommateur du composant n'a pas connaissance.

Boite noire vs. boite blanche

Pour bien comprendre le principe d'une boite noire, pensez par exemple à votre autoradio CD : vous lui donnez en entrée un CD, et il est censé le lire, c'est à dire envoyer vers vos enceintes un signal analogique correspondant aux morceaux de musiques qui ont été gravés sur ce disque. Il le fait… comme il veut : vous vous moquez de savoir qu'à l'intérieur de la boite, beaucoup d'opérations sont réalisées (faire tourner le disque, déplacer la tête de lecture, activer la diode laser, renvoyer le faisceau à angle droit via un prisme pour le diriger vers les lentilles, détecter les variations de lumière reçues via une diode photosensible, etc.). Vous voulez simplement que quand vous insérez un disque de Jimi Hendrix que que vous appuyez sur Lecture, les chansons de Jimi Hendrix soient jouées.

Où tester ?

Nous le verrons plus tard, il existe différentes stratégies pour implémenter du code de test unitaire, qui dépendent de vos conventions personnelles, des frameworks que vous utilisez, etc.

Même si nous verrons qu'il est possible, dans certains langages et frameworks, d'écrire des tests directement dans le code applicatif (par exemple avec les Doctests en Python), cette stratégie est généralement limitée aux modules de petite envergure, et très autonomes.

Important Dans la majeure partie des cas, le code de tests sera isolé du code applicatif. On écrira par exemple, dans les langages objet, des classes de test.

Quand tester ?

Voici une autre question importante, et dont la réponse n'est pas universelle…

Tout d'abord, retenons que l'important est de tester, et qu'il n'est de toute façon jamais trop tard pour bien faire : si vous avez un projet en production qui ne comporte aucun test unitaire, vous pouvez très bien en introduire progressivement, rien n'est perdu.

Le TDD : développement dirigé par les tests

Une démarche intéressante est le TDD pour Test Driven Development, ou développement dirigé par les tests. C'est aussi un concept « à la mode » : tout le monde en parle, mais peu de développeurs l'utilisent réellement et efficacement !

Et pourtant, agilité rime avec TDD : quand on veut livrer souvent, vite et bien, l'une des meilleures stratégies est de se laisser guider par le contrat de service de nos composants : autrement dit, on commence par écrire les tests, on les regarde échouer lamentablement, puis on écrit le code et… on espère qu'ils n'échouent plus.

Testez vos connaissances

Idéalement, que faut-il tester ?
  • Tout les composants que l'on développe, avec une couverture de test raisonnable
  • Tous les composants que l'on utilise
  • Seulement quelques composants que l'on teste à 100% de couverture de test

Rien ne sert de tester les composants tiers que vous utiliser : si vous avez choisi des composants fiables, c'est déjà fait ! Par exemple, si vous utilisez le framework de développement Spring, vous ne vous amuserez jamais à tester unitairement son composant Spring-MVC : les créateurs de Spring l'ont fait pour vous.

Quant à vos propres composants, testez tout, quitte à ce que vos tests soient partiels.

Sachant que l'on cherche un niveau de qualité optimal, pourquoi ne pas viser une couverture de test de 100% ?
  • Parce qu'un getter a peu de chance d'être buggué.
  • Parce que pour certains composant, le testing unitaire présente un ROI très limité.
  • Parce que c'est impossible !