OpenClassrooms

Les tests unitaires en Java

OpenClassrooms
En Ligne

Prix sur demande
Ou préférez-vous appeler directement le centre?

Infos importantes

Typologie Formation
Méthodologie En ligne
  • Formation
  • En ligne
Description

Grâce à cette formation vous pourrez acquérir les connaissances nécessaires qui vous permettrons d’ajouter des compétences à votre profil et obtenir de solides aptitude qui vous offriront de nombreuses opportunités professionnelles.

Questions / Réponses

Pose une question et d'autres utilisateurs vous répondrons

Qui voulez-vous pour répondre à votre question ?

On publiera seulement ton nom et prénom et ta question

Programme

Introduction du cours

Bonjour à tous,

Ceci est mon premier tutoriel, tous les commentaires sont les bienvenus. Je vais vous parler des tests unitaires en Java. Nous allons voir d'abord un peu de théorie sur les tests puis nous verrons comment en créer avec JUnit. Enfin nous verrons comment évaluer la couverture de nos tests.

Définitions et utilité

Vous qui programmez en java depuis un moment déjà, je suis sûr qu'il vous est déjà arrivé d'avoir un bug dans votre programme et de ne pas savoir d'où il venait. Votre algorithmes est juste, votre cascade d'appel de méthode, d'instanciation d'objet marchent, il n'y a pas d'exception qui apparaît. Et pourtant. Pourtant ça ne marche pas. Votre code fait mille lignes ou plus, il est complexe et une méthode au moins bug. Vous ne savez pas laquelle.
Les tests sont faits pour cela. Ils vont vous aider à définir où est le problème.
Il existe plusieurs types de tests :

  • Les tests d'intégration : le programme créé s'intègre-t-il bien dans son environnement d'exécution ?

  • Les tests d'acceptation : l'utilisateur final accepte-t-il le logiciel ?

  • Les tests unitaires : destinés à tester une unité du logiciel.

Ce sont ces derniers qui nous intéresseront et les unités que nous allons tester seront les méthodes de nos classes.

Voici un exemple simple : soit cette méthode String concatene(String a, String b) {...}. Nous voulons tester si elle concatène bien les deux chaînes a et b. La première méthode est de la tester dans notre programme, on appelle cette méthode dans le main avec deux chaînes et on affiche le résultat. Le premier qui fait un truc comme ça après avoir lu ce tuto, je le cloue.
L'autre méthode consiste à créer une classe dédiée à ce test, c'est précisément le but du test unitaire. Mais voyons pourquoi la première méthode est à bannir pour de bon.

test unitaire

test perso

reproductible

oui

non

compréhensible

oui

non

documenté

oui

non

conclusion

bon pour le service

à bannir

J'espère maintenant vous avoir convaincu que le test unitaire est utile. Il y a peut être encore un point qui est discutable : le temps de mise en place des tests. Tous ceux qui ont déjà eu à traiter un bug bien caché le savent, ce genre de bug est long et pénible à trouver.

Mais si un test est long et pénible à écrire, on ne gagne rien.

C'est vrai. Mais vous verrez qu'un test est simple à écrire dans la plupart des cas et qu'il n'est pas pénible du tout : on écrit les tests pour une seule méthode ! Pas besoin de savoir exactement ce que vaut le paramètre xy de la sous-classe alpha situé dans un autre package que celui où on est maintenant.

En réalité, on va créer une classe de test par classe à tester. Dans chaque classe de test, il y aura une méthode par méthode à tester. Donc en fait, pour chaque classe du logiciel, on va avoir sa sœur pour le test.

Mais définissons tout d'abord notre objectif. Notre objectif est de trouver un maximum de bug. Pourquoi pas tous ? Parce que ce serait trop long et trop difficile, il faudrait être sûr que dans tous les cas, si un certain nombre de préconditions sont remplies, alors un certain nombre de post-conditions le seront. Toujours. Quoiqu'il arrive.
C'est parce que prouver que son logiciel est exempt de bug est trop difficile que nous allons seulement mettre en place un moyen de trouver quelques bugs (mais bien sur, si on trouve tous les bugs, on ne va pas se plaindre).
Pour tester, nous allons nous baser sur deux assomptions :

  • Si ça marche une fois, ça marchera les autres fois;

  • Si ça marche pour quelques valeurs, ça marchera pour toutes les autres.

Ces deux assomptions réduisent drastiquement le nombre de cas de test à effectuer.

Définition : un cas de test est un ensemble composé de trois objets.

  • Un état (ou contexte) de départ;

  • Un état (ou contexte) d'arrivée;

  • Un oracle, c'est à dire un outil qui va prédire l'état d'arrivée en fonction de l'état de départ et comparer le résultat théorique et le résultat pratique.

Un cas de test peut donc s'appliquer à plusieurs méthodes, par exemple plusieurs classes implémentant la même interface.

Voici (enfin) un exemple de test. Cet exemple ne respecte volontairement pas les notations que nous allons utiliser pour simplifier la chose.

public boolean concateneTest() { MyString classATester = new MyString(); String a = "salut les "; String b = "zeros"; String resultatAttendu = "salut les zeros"; String resultatObtenu = classATester.concatene(a, b); if (resultatAttendu.compareTo(resultatObtenu) == 0) { return true; } else { return false; } }

Nous pouvons observer plusieurs choses de ce bout de code :

  • Le test ne dit pas quelle est l'erreur, il dit seulement qu'il y en a une;

  • Le test ne corrige pas l'erreur;

  • Ce n'est pas parce que le test passe qu'il n'y a pas d'erreur;

  • Ce n'est pas parce que vous corriger l'erreur qu'il n'y en a plus.

Bon passons enfin à la pratique.

Mise en pratique

Bon, nous avons vu ce qu'était un test et pourquoi en faire. Nous allons maintenant voir comment les mettre en pratique grâce à JUnit, le framework de test unitaire de Java. Bien que JUnit soit intégré à la plupart des IDE, il ne fait pas partie de la librairie standard de Java. En fait, JUnit est le framework de test unitaire qui fait partie d'un plus grand ensemble nommé XUnit. XUnit désigne tous les frameworks de test répondant à certains critères pour une multitude de langage. Il y a par exemple CUnit pour le C, CPPUnit pour le C++ ou encore PHPUnit pour PHP. Et la liste est longue.

Comme je ne connais pas NetBeans, je ne pourrai pas décrire les manipulations pour cet IDE. Mais ce tutoriel n'est pas à propos de la configuration d'un IDE et vous ne devriez pas avoir de problème à vous adapter. Tout ce que je montrerai, les captures d'écran et la navigation dans les menus seront donc les manipulations à faire sous Eclipse. J'utilise Eclipse Indigo, il y a peut être de petites différences avec les autres versions.

Comme je vous l'ai dit dans la première partie, nous allons avoir pour chaque classe à tester, sa classe sœur. Pour simplifier la maintenance du code et le packaging de notre logiciel, nous allons créer deux packages principaux : main et test. Dans main, nous mettrons toutes nos classes pour le logiciel et dans test, nos classes de test.

Il faut encore que je vous dise une chose à propos des tests : il y a des tests boite noire (black-box) et des tests boite blanche (white-box). Les tests boite noire se font sans que le testeur ne connaisse le contenu de la méthode qu'il va tester alors que les tests boite blanche donne accès au contenu de la méthode à tester.
Les deux ont leurs avantages et inconvénients : lorsque l'on teste en boite noire, on teste réellement ce que devrait faire la méthode. Lorsque l'on teste la méthode en connaissant son fonctionnement. Le risque est alors de tester le fonctionnement et d'oublier le but final de la méthode. En contre-partie, nos tests pourront être plus précis.

Nous allons développer une classe qui permettra de calculer le résultat d'opérations mathématiques de base. Voici l'interface de la classe :

package main; public interface Calculator { int multiply(int a, int b); int divide(int a, int b); int add(int a, int b); int substract(int a, int b); }

Cette interface est très simple, on opère seulement sur des entiers. Pas d'autres restrictions.
J'ai écris cette interface en quelques minutes, juste pour vous ;) . Il y a un avantage à l'avoir écrite, en dehors du fait que programmer par interface est une bonne chose : vous qui êtes testeurs professionnels (ou le serez dans peu de temps) vous pouvez développer le test pendant que je m'occupe de l'implémentation. Certaines techniques de développement préconisent même d'écrire tous les tests avant de commencer le logiciel. Au fur et à mesure du développement, de plus en plus de tests vont réussir et lorsqu'ils réussissent tous, le logiciel est terminé. C'est la méthode de développement dite "test-driven".

Générer les tests

Maintenant, créez un nouveau projet, copiez l'interface que je viens de vous donner dans le package main et créez un package test.
Créez la classe CalculatorImpl qui implémente Calculator, générez les méthodes mais ne les remplissez pas encore, j'ai une surprise pour vous. :p
Nous allons maintenant écrire nos tests. Pour cela, cliquez droit sur le package test (que vous aurez créer en faisant un nouveau projet) et faites new>>Junit test case. Nommez votre classe de test, habituellement, si vous testez la classe Zeros, la classe de test s'appelle ZerosTest ou bien TestZeros. Pour notre exemple, la classe à tester s'appellera CalculatorImpl et la classe de test sera CalculatorImplTest.
Voici ensuite les informations pour Eclipse : nous ne voulons pas créer les méthodes setUp et tearDown, ces méthodes servent à générer le contexte dans lequel votre classe doit s'exécuter, établir les connexions à la base de donnée par exemple, laissez donc les quatre cases décochées. Cliquez ensuite sur suivant. Vous voyez alors les méthodes disponibles au test, cochez seulement celles qui sont définies dans notre classe. On imagine que les méthodes définies dans les super-classes sont déjà testées. Cliquez sur terminé et voici notre classe de test. Vous devriez être arrivé à cela :

package test; import static org.junit.Assert.*; import org.junit.Test; public class CalculatorImplTest { @Test public final void testMultiply() { fail("Not yet implemented"); // TODO } @Test public final void testDivide() { fail("Not yet implemented"); // TODO } @Test public final void testAdd() { fail("Not yet implemented"); // TODO } @Test public final void testSubstract() { fail("Not yet implemented"); // TODO } }

Alors que voyons-nous ici ? Eclipse a généré pour nous quatre méthode destinées à tester les quatre méthodes de la classe CalculatorImpl. Bien sûr, Eclipse ne peut pas générer le contenu des tests, le code est donc seulement fail("Not yet implemented"); // TODO afin de faire échouer un test non écrit. En plus l'IDE nous laisse un petit message pour nous préciser la raison de l'échec.

Hey ! c'est ça veut dire quoi import static org.junit.Assert.*; ? J'avais jamais vu d'import statique.

Un import statique permet de faire appel à des méthodes statiques sans préciser le nom de la classe qui définit ces méthodes. Voici un exemple avec la classe Math, si nous avons :

import static java.lang.Math.cos; import static java.lang.Math.PI;

Alors ceci est valide :

double d = cos(2*PI);

Sans les imports statiques il aurait fallu faire :

double d = Math.cos(2*Math.PI);

Et pour cette explication sur les imports statiques, je dois un grand merci à Ruby Elegance. Ce que j'ai écrit ici est presque un copier-coller de la discussion que nous avons eue lorsque que le tutoriel était en bêta-test.

Vous pouvez maintenant lancer le test en cliquant droit sur notre classe nouvellement créée, puis run as et JUnit test. Un nouvel onglet apparaît alors vous indiquant que les quatre tests ont échoués. Dans le bas de l'onglet, une raison est affichée, c'est celle qui est donnée en paramètre de la méthode fail().
Voici l'image de l'onglet qui est apparu. J'ai entouré en rouge les deux informations intéressantes.

Les tests échouent

Et voila ! Vous avez créé votre premier test !
Nous allons maintenant l'étoffer un peu. La première chose à faire, histoire d'être un peu sérieux, c'est d'implémenter notre classe CalculatorImpl.
Et voici ma surprise :

Pour implémenter cette interface, vous n'aurez pas le droit d'utiliser les opérateurs +, -, * et /. De cette façon, les méthodes que nous allons tester ne seront pas trop triviales. Par contre, vous pouvez utiliser ++ et --, vous pouvez utiliser les tests (==, <, >, ...), vous pouvez aussi utiliser l'opérateur - en tant qu'opérateur unaire, c'est à dire pour transformer 5 en -5 par exemple.

Voici par exemple ma méthode pour l'addition, la plus simple. Je vous la donne au cas ou vous n'auriez pas d'idée. Cependant, le but de ce TP est que vous fassiez des erreurs de programmation et cette classe passe mes tests. Même s'il ne sont pas à toute épreuve, cette méthode ne devrait pas comporter de faute, je vous conseille donc de ne pas la regarder.

@Override public int add(int a, int b) { int res = a; if (b > 0) { while(b-- != 0) { res++; } } else if (b < 0) { while(b++ != 0) { res--; } } return res; } Remplir les méthodes de test

Créons donc le test qui va avec. Pour les tests, je vous autorise à utiliser tous les opérateurs que vous souhaitez. Vous avez vu que la méthode fail() fait échouer le test, nous allons donc l'utiliser à chaque fois qu'un test échoue.

Pour écrire un test correct, nous avons vu que nous n'allions tester que quelques valeurs puis que nous allions généraliser. Cependant, nous n'allons pas choisir ces valeurs au hasard et c'est là que réside tout l'art d'écrire un test.

Nous avons donc nos arguments a et b. Nous allons les additionner, à priori pas de problème. Mais nous allons quand même tester plusieurs cas spéciaux :
si a ou b ou les deux est (sont) négatif(s) ou nul. Bien sûr, nous allons aussi tester s'ils sont positifs.

D'une manière générale, lorsque vous écrivez un test, il faut tester avec quelques valeurs standards, qui n'ont pas de signification...


Comparer pour mieux choisir:
En voir plus