C306 Tests Unitaires
C306 Tests Unitaires
C306 Tests Unitaires
id=7736
Tests unitaires
1 - Introduction
Pour être sûr que le code fait bien ce qu'on en attend, il faut le tester. Pour cela, il faut écrire des tests dont les résultats sont connus et qui
éprouveront le code.
Exemple
Les tests appliqués aux fonctions/méthodes sont appelés "tests unitaires". Pour valider les fonctions, il faut donc créér une batterie de tests
unitaires. Si les tests sont complets, il est aisement possible de modifier le code ("refactoring") sans crainte de mettre en péril le projet (en
assurant la "non-regression"). La solidité du projet tient donc dans l'exaustivité des tests. Dans l'idéal, les tests unitaires sont écrits avant
l'implémentation des fonctions (Test Driven Development/développement piloté par les tests).
Exemple
Associé aux outils de couverture de code (qui permettent de signaler les parties de code non atteint par une exécution), les tests complets
permettent aussi de fournir les parties de code qui ne sont pas accessibles (ou accédées ?).
Comme il est difficile voir souvent impossible d'avoir des tests qui couvrent tous les cas possibles, il est important d'être très attentif à leur
écriture et de n'avoir que des fonctions simples (donc faciles à tester).
2 - Notions à assimiler
• Test unitaire
• Non-régression
• Refactorisation / Refactoring
Par exemple modifier le code pour en améliorer les performances correspond à une "refactorisation".
“Le Test Driven Development (TDD) ou en Français développement piloté par les
tests est une technique de développement de logiciel qui préconise d'écrire les
tests unitaires avant d'écrire le code source d'un logiciel. (Wikipédia,
l'encyclopédie libre)”
https://junit.org/junit5/docs/current/user-guide/#running-tests-console-launcher
assertEquals(4,min(4,5)); ?
sera valide si le résultat de l'appel à la méthode 'min(4,5)' correspond au résultat attendu '4'.
Si l'exécution du test conduit à un état non valide, on peut aussi utilisé la méthode 'fail' pour déclencher la non validation du test (voir la partie
sur les exceptions).
Toutes les informations utiles sur ces méthodes se trouve dans la 'Javadoc' de JUnit
https://junit.org/junit5/docs/current/api/org/junit/jupiter/api/Assertions.html
3.2 - Ecriture d'un test JUnit
Le test peut être écrit directement dans la classe, avec l'annotation @Test précédent la méthode de test.
1 @Test ?
2 public void testMin() {
3 assertEquals(4,min(4,5));
4 }
Cette méthode de test est écrite dans une classe java qui sera dédiée à un jeu de tests:
1 /** ?
2 * Classe d'exemple
3 * @author [email protected]
4 */
5 public class Foo {
6
7 public static int min(int a,int b){
8 if ( a < b )
9 return a;
10 return b;
11 }
12
13 }
Compilation de la classe :
N.B.: sous windows, le séparateur de dossier dans le "classpath" est ';' et non ':', il faudrait donc faire :
Exécution des tests: (il va prendre toutes les classes qui contiennent 'Test')
╷
├─ JUnit Jupiter ✔
│ └─ TestFoo ✔
│ └─ testMin() ✔
└─ JUnit Vintage ✔
Toutes les méthodes annotées "@Test" ont été exécutées (ici 1), le résultat est "successful"
Recompiilation
Exécution du test
╷
├─ JUnit Jupiter ✔
│ └─ TestFoo ✔
│ ├─ testMin() ✔
│ └─ testMin2() ✘ expected: <2> but was: <4>
└─ JUnit Vintage ✔
Failures (1):
JUnit Jupiter:TestFoo:testMin2()
MethodSource [className = 'TestFoo', methodName = 'testMin2', methodParameterTypes = '']
=> org.opentest4j.AssertionFailedError: expected: <2> but was: <4>
org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:55)
org.junit.jupiter.api.AssertionUtils.failNotEqual(AssertionUtils.java:62)
org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:150)
org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:145)
org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:510)
TestFoo.testMin2(TestFoo.java:17)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.base/java.lang.reflect.Method.invoke(Method.java:566)
[...]
Le test n'a pas été validé ("failed"), en retour JUnit renvoie une erreur donnant le résultat qui était attendu (2) et le résultat obtenu (4) ainsi que
la trace d'exécution de l'erreur.
Cela peut être réalisé avec les instructions de gestion des exceptions try {} catch{} : on provoque l'exception dans le bloc 'try', le résultat
attendu étant que l'exception soit levée, l'exécution doit se poursuivre dans le bloc 'catch' et non continuer dans la suite du bloc "try".
Ainsi un appel à 'fail' après l'instruction ayant dû provoquer l'exception mettra le test en échec si celle-ci n'est pas levée.
Exemple
Exécution du test
╷
├─ JUnit Jupiter ✔
│ └─ TestDivisionParZero ✔
│ └─ testDivZero() ✔
└─ JUnit Vintage ✔
4 - Exercices
Exercice
Ecrire un test permettant de contrôler le bon fonctionnement de la méthode suivante. La question ici n'est pas d'implémenter la méthode mais
d'écrire les tests.
1 /** ?
2 * Calcul le PGCD (Plus Grand Denominateur Commun)
3 * de 2 nombres positifs ou nul.
4 *
5 * @param a premier entier
6 * @param b second entier
7 * @return pgcd de a et b
8 * @throws IllegalArgumentException si a ou b est strictement négatif
9 **/
10 public static int pgcd(int a,int b){
11 // TODO
12 }
Solution
1 @Test ?
2 public void testPgcd() {
3 assertEquals(8,pgcd(24,16));
4 assertEquals(10,0,10);
5 assertEquals(0,0,0);
6 }
7
8 @Test
9 public void testPgcdArguments1() {
10 try {
11 pgcd(1,-1);
12 fail("exception pas detectée sur a");
13 } catch (IllegalArgumentException e) {
14 // ok
15 }
16 }
17
18 @Test
19 public void testPgcdArguments2() {
20 try {
21 pgcd(-1,1);
22 fail("exception pas detectée sur b");
23 } catch (IllegalArgumentException e) {
24 // ok
25 }
26 }
Conclusion
Les tests permettent un contrôle automatisé du fonctionnement attendu des fonctions/méthodes. Pour pouvoir facilement écrire les tests, il
est préférable de penser aux tests avant l'implémentation.
Les exemple ci-dessus manipulaient des méthodes statiques, mais rien n'empêche de reproduire la même chose avec des instances de classes
et leurs méthodes:
◄ Modularité
Aller à…
Couverture de code ►