Autour du codeDevelopper toujours mieux
Posté le

Philosophie des tests unitaires

Si aujourd'hui il existe une pléthore de frameworks pour nous aider à écrire des tests unitaires. Encore faut-il savoir comment tester.

Avant de commencer cet article il me semble important de rappeler quelque base sur ce qu'est un logiciel écrit avec des principes orientés objets et donc de rappeler ce qu'est un objet.

Un objet est un ensemble qui contient des données. La valeur des données définissent l'état de l'objet. Pour que l'état soit toujours cohérent, on ne peut modifier directement les données mais, on utilise des méthodes définies dans l'objet qui le font à notre place. C'est le principe d'encapsulation. Toute la mécanique est cachée dans l'objet

Si vous avez un objet mixeur, vous allez avoir trois méthodes.

  • Mettre les aliments dedans
  • Mixer
  • Récupérer les aliments

L'ensemble des choses que l'on peut faire avec un objet s'appelle son interface. La façon dont l'objet réalise ces choses s'appelle son implémentation. Ainsi, l'interface répond à la question que fait-il et l'implémentation à la question comment le fait-il. La plupart des langages de programmation ne font pas de séparation stricte entre interface et implémentation, ces deux concepts sont regroupés dans la classe.

Programmer pour une interface et non une classe. Quand vous programmez vous ne devez pas vous demander comment fonctionne cet objet mais, que puis-je faire avec ? S'il répond à mes besoins, je peux l'utiliser...

C'est pareil lorsque vous testez un objet. Regardez son interface et testez si son implémentation respect son interface.

Si je vous donne un mixeur à tester mettez des aliments dedans, mixer, récupérer les aliments et vérifiez que vos aliments sont bien mixés. C'est ce que l'on appelle le cas autoroute c'est le cas d'utilisation le plus normal que l'on puisse faire pour faire de bon testes, il faut malmener votre objet. En s'intéressant uniquement à l'interface du mixeur, on peut faire les tests suivants:

  • Vérifier que si je mets mes aliments dedans et que je ne le lance pas je puisse récupérer mes aliments non mixés
  • Vérifier que si je lance mon mixeur à vide, je récupère du vide à la fin.

Ce qu'il ne faut surtout pas faire, vérifier que lorsque je lance mon mixeur, cela entraine son moteur électrique qui va entrainer la lame.

L'interface de l'objet mixeur ne vous dis pas que l'on peut regarder son moteur elle ne dit même pas qu'il a un moteur. Beaucoup de langages permettent de rendre inaccessible tout ce qui ne fait pas parti de l'interface grâce au scope privé. Ce n'est pas le cas de python même si une convention existe, faire commencer par un _ tout ce qui ne fait pas partie de l'interface.

Problème de composition

En programmation orientée objet, il y a l'instanciation, l'étape où l'on va construire un objet.l'instanciation fait parti de l'interface de l'objet, (sauf cas très particulier des classes imbriqués),Ainsi on pourrait avoir un mixeur qui s'instancie avec un moteur. Ce qui permet d'avoir des mixeurs avec des moteurs de différentes puissances. L'interface d'un moteur est très simple. démarrer arrêter

Concernant les testes de notre mixeur il va falloir les modifier pour que lorsque l'on construise un mixeur on lui passe un moteur mais quel moteur choisir ? Doit-on tester notre mixeur avec tous les moteurs ? undecided Non du moment que l'on a testé que l'implémentation de nos moteurs respectent leur interface et que l'on a programmé notre mixeur pour qu'il fonctionne avec l'interface d'un moteur, pas de problème. cool

Donc on en choisit un au pif ?

On peut également utiliser un simulacre. Les simulacres ou mock sont des objets utilisés dans les tests qui sont censés respecter l'interface de l'objet simulés, ils n'ont pas grosse intelligence et l'on fait généralement en sorte que les appels à une de leur méthode retourne une valeur fixe.

Vous étes le héro de ce test: J'utilise des mocks ou J'utilise un moteur au pif pour les tests, à vous de choisir.

J'utilise des mocks

Créons un simulacre de moteur puis on va vérifier Que lorsqu'on lance le mixeur, la méthode démarrer du moteur est appelée une fois. Puisque lorsqu'on appelle la méthode récupérer les aliments du mixeur, la méthode arrêter du moteur est appelée.

Sur les 10 implémentations de mixeurs que vous avez testé 2 ne passe pas. L'un des mixeurs lance et arrête 3 fois le moteur pour permettre aux aliments de retomber et de mieux être mixée.

L'autre mixeur n'appelle pas la méthode, arrêter, à l'ouverture du mixeur il l'appelle une seconde après avoir lancé le moteur. Il met deux secondes à ouvrir le mixeur pour être sûr que le moteur soit arrêté.

Vous adaptez vos testes pour ces deux mixeurs vous vous rendez compte que vos testes ne sont plus fait pour une interface, mais une implémentation en effet vous avez du vous poser la question comment marche mon mixeur pour pouvoir adapter vos teste. Est-ce vraiment mal ? Une malédiction va-t-elle s'abattre sur vous ? Peut importe maintenant que vos teste passe vous n'avez plus le temps de revenir en arrière. Allez au paragraphe Le programme évolue

J'utilise un moteur au pif

Vous prenez un moteur au pif pour chacune des dix implémentations du mixeur. Vos tests passent, tout va pour le mieux dans le meilleur des mondes. Allez au paragraphe Le programme évolue

Le programme évolue

Petit à petit vous forgé votre expérience en écriture de testes vos moteurs sont utilisés dans deux autres types d'objets, des machines à laver et des sèches linges. On vous demande de concevoir un ventilateur avec deux vitesse différentes. On souhaite que le moteur est 3 états:

  • arrêté
  • lent
  • rapide

Vous adaptez les implémentations de vos moteurs et les testes unitaires de ceux-ci. Rendez-vous au paragraphe J'ai utilisé des mock ou J'ai utilisé un moteur au pif en fonction de votre choix précédent.

J'ai utilisé des mock

vous relancez l'ensemble de vos tests unitaires et tout est au vers vous ne voyez pas l'armée de bugs arrivés sournoisement.

Aucun des mixeurs avec les nouveaux moteurs ne fonctionne, vous n'avez pas pensé à adapter vos simulacres. Le langage de scripte utilisé n'indiquant pas de façon formel que vos simulacres doivent respecter l'interface de l'objet qui simule, quand l'interface change les mocks n'en savent rien et les bugs arrivent. Rendez-vous au paragraphe Fin de l'aventure

J'ai utilisé un moteur au pif

vous relancez l'ensemble de vos tests unitaires et remarquez que tout est au rouge sauf les tests sur les moteurs. Vos tests vous indiques qu'aucun mixeur ne peut comprendre la nouvelle interface des moteurs. Vous modifiez tous vos mixeurs et relancez les tests, tous est au vert. Rendez-vous au paragraphe Fin de l'aventure

Fin de l'aventure

Comme on peut le voir ici, une des principales plus-value des tests unitaires et de se prémunir contre les régressions et de connaitre l'impact d'une modification sur les autres composants. Il faut cependant garder à l'esprit que les tests peuvent prouver la présence de bugs, mais pas l'absence.

La quête de la couverture de code en pervertie plus d'un

La couverture de code, c'est la quantité d'instruction dans le code qui est exécuté lorsque les testes tournent.

Si toutes les instructions sont exécutées, vous avez une couverture de cent pour cent. Il existe des logiciels qui permettent de connaitre cette couverture comme coverage pour python ou cobertura pour Java. Ces logiciels marchent très bien mais, utiliser leur métrique peut amener à certaine dérive, en effet ce n'est pas parce qu'un code à une bonne couverture de tests que les tests sont pertinents. Voici un exemple pour vous en convaincre avec la méthode faire ses courses:

class: Humain
   methode: FaireSesCources(magasin)
        this.entrerDansMagasin(magasin)
        this.sortirDuMagasin(magasin)

   methode: entrerDansMagasin(magasin)
        du code...

   methode: sortirDuMagasin(magasin)
        encore du code ...

Je me fixe pour seul objectif les 100% une façon très simple de les atteindre est la suivante.

class: TestHumain:
   homme: h1, h2
   magazin: m1, m2

   h1.FaireSesCource(m1)

   h2.entrerDansMagasin()
   h2.sortirDuMagasin()

   h1 == h2 ?

Voila, chaque instruction est même appelé deux fois par ce test, mais la pertinence est lamentable. Un test pertinent aurai été de vérifier si h1 à moins d'argent est plus de provision après avoir fait ses courses. De plus si une modification sur entrerDansMagasin ou sortirDuMagasin incorpore un bug, cette régression ne sera pas détectée et pourtant la couverture ne pouvait pas être meilleure.

Autre chose, votre code est semblable à un graphe ou chaque structure conditionnelle ou boucle représente un noeud du graphe.

if a:
    # instruction 1
if b:
    # instruction 2

Ici un test avec "a" et "b" à vrai couvre toute les instructions, mais que se passe-t-il si l'on exécute seulement l'instruction 2 sans avoir exécuté l'instruction 1 ? Avoir une couverture totale du code ne garantit pas que l'on est passé par tous les noeuds du graphe, mais pas que l'on a testé tous les chemins possibles. Donc ne vous laissez pas impressionner par cette métrique qui n'est pas une preuve de la qualité du code, mais inquiétez vous si elle est au ras des pâquerettes, c'est le signe que des régressions passeront inaperçue.

Tout tester n'a pas d'intérêt

Ne perdez pas votre temps à tester des constantes. Il n'y a aucune logique, c'est simplement du déclaratif écrire des tests pour ça est tellement stupide qu'un script awk pourrait faire le job à votre place. Sérieusement ? Vous allez vraiment copier le retour constant d'une méthode pour le coller dans un test ? Si vous avez du temps à perdre, écrivez deux fois votre programme et faites un diff se sera encore mieux. Evitez également d'être trop tatillons avec les testes si vous testez une sortie ou un affichage, vérifié seulement que les informations qui doivent être affichées sont présentes. Rien de plus pénible que des tests qui pètent dès que l'on ajoute une pauvre virgule ou un soulignage.

Les tests en dos à dos

Une dernière astuce, pour tester les algorithmes, souvent on oublie un cas particulier dans lequel l'algo ne marche pas. Il existe une technique particulière pour vous aider face à cette problématique, le dos à dos. Elle consiste à créer un autre algorithme que fait la même chose que l'algorithme à tester, mais de la façon la plus stupide qui soit, peu importe que ses performances soient désastreuses. Par exemple, un algorithme stupide de collision entre deux formes géométriques prend tous les points un à un de la forme 1 et regarde s'ils correspondent à un point dans la forme 2.

Notre test va générer des entrées aléatoires et les donner à manger à nos deux algorithmes puis comparer les deux sorties. il s'arrête à chaque erreur. On regarde quelles données d'entrées ont provoqué l'arrêt du test puis on crée un test spécifique avec ce cas particulier. On corrige notre algorithme et on recommence. Ca permet de tester des choses complexes avec peu d'effort.

Voilou j'espère qu'après ça, vous aurez une idée plus claire sur les pièges à éviter lors de l'écriture de tests, alors vos IDE et bon code laughing