:: Enseignements :: ESIPE :: E4INFO :: 2021-2022 :: Java Avancé ::
[LOGO]

Examen de Java Avancé - Session 2


Le but de ce TP noté est d'implanter une classe UnitTest qui permet de définir et d'exécuter des tests unitaires (un peu comme JUnit, mais en beaucoup plus simple).

Vos sources Java produites pendant l'examen devront être placées sous le répertoire EXAM de votre compte ($HOME) (qui est vide dans l'environnement de TP noté). Sinon, elles ne seront pas récupérées.

Tout document papier est proscrit.
La javadoc 17 est https://igm.univ-mlv.fr/~juge/javadoc-17/api/index.html.
Les seuls documents électroniques autorisés sont les supports de cours à l'url http://igm.univ-mlv.fr/~forax/ens/java-avance/cours/pdf/.

Vous avez le droit de lire le sujet jusqu'au bout, cela vous donnera une bonne idée de là où on veut aller !

Exercice 1 - UnitTest

UnitTest est une classe qui permet d'enregistrer un ensemble de tests. Chaque test est défini par un nom name ainsi que plusieurs actions. Une action est une fonction qui ne prend pas d'argument et qui ne renvoie pas de valeur. L'action va être considérée comme réussie si elle ne lève pas d'exception quand on l'exécute et elle sera considérée comme un échec sinon.
Voici un exemple d'utilisation :
  var unitTest = new UnitTest();
  unitTest.test("test0",
    () -> List.of("Picsou").get(0),
    () -> List.of("Picsou").get(1)
  );
  unitTest.test("test1",
    () -> ensure(34).equalsTo(37),
    () -> ensure(12).not().equalsTo(42)
  );
  unitTest.test("test2",
    () -> ensureCode(() -> 42).returnValue().equalsTo(42),
    () -> ensureCode(() -> { throw null; }).throwsAnException(NullPointerException.class)
  );
  var report = unitTest.runAll();
  System.out.println(report);
    // Report[names=[test0, test1, test2], errors={test1=[java.lang.AssertionError: 34 is not equal to 37], test2=[], test0=[java.lang.AssertionError: java.lang.IndexOutOfBoundsException: Index: 1 Size: 1]}]
     
Dans l'exemple ci-dessus, on créé trois tests appelés test0, test1 et test2 avec la méthode test(). Chaque test contient deux actions qui permettent d'exécuter le code de test. La méthode runAll() exécute tous les tests définis et stocke le résultat des tests dans un objet Report. Cet objet contient une liste des noms des tests (names) ainsi qu'une table associative pour chaque test indiquant la liste des erreurs (de type java.lang.Error) qui se sont produites pour les actions de ce test. Par exemple, le test0 a une action qui échoue (la seconde, car l'index 1 n'existe pas dans une liste de taille 1).
La méthode ensure() prend en paramètre une valeur et on peut ensuite tester si deux valeurs sont égales avec equalsTo() ou différentes avec not().equalsTo(). Dans l'exemple, le test1 a une action qui échoue (la seconde, car 34 n'est pas égal à 37).
La méthode ensureCode() prend en paramètre une fonction qui renvoie une valeur et on peut soit tester la valeur de retour comme précédemment grâce à returnValue ou vérifier si la fonction lève une exception (checked ou non) avec throwsAnException(). Dans l'exemple, le test2 n'a pas d'action qui échoue (dans le second cas, c'est bien l’exception attendue qui est levée).

La classe UnitTest possède les méthodes suivantes :
  • checkEquals() pour tester si deux valeurs sont égales, on va utiliser cette méthode avant que la méthode ensure ne soit écrite.
  • ensure() qui permet de tester deux valeurs.
  • ensureCode() qui permet de tester la valeur de retour d'une fonction ou si une fonction lève une exception.
  • test() qui permet de définir un nouveau test avec un nom et des actions.
  • testCount qui indique le nombre de tests définis.
  • runOnly() qui prend en paramètre un nom de test, exécute celui-ci et renvoie une liste des erreurs résultant de l'exécution de chaque action du test (l'ordre de la liste des erreurs doit être le même que l'ordre des actions fournies à la méthode test).
  • runAll() qui exécute tous les tests et retourne un objet Report qui indique le nom de tous les tests ainsi que pour chaque test, la liste des erreurs résultant de l'exécution de chaque action du test.

Des tests unitaires correspondant à l'implantation sont ici : UnitTestTest.java.

  1. Dans un premier temps, écrire dans la classe UnitTest du package fr.uge.test, la méthode statique checkEquals qui teste si deux objets sont égaux (ça doit aussi fonctionner si les deux objets sont null).
    La méthode doit lever une AssertionError avec le message "<value1> is not equal to <value2>" si les deux valeurs ne sont pas égales et ne rien renvoyer sinon.
    Pour simplifier, on va se limiter pour l'instant à définir des tests avec un nom et une seule action. Par ailleurs, il ne doit pas être possible d'avoir deux tests ayant le même nom.
    Ajouter de quoi construire un UnitTest, puis écrire la méthode test() et la méthode testCount() qui renvoie le nombre de tests définis.
    Vérifier que les tests marqués "Q1" passent.

  2. Écrire la méthode runOnly() qui prend en paramètre un nom de test et exécute l'action de celui-ci ; elle renvoie une liste qui peut soit être vide soit, soit contenir l'erreur produite par l'exécution de l'action du test.
    Dans le cas où il n'existe pas de test avec le nom en argument, une exception doit être levée.
    Vérifier que les tests marqués "Q2" passent.

  3. En fait, lorsque l'on exécute l'action d'un test, celui-ci peut lever plusieurs types d'exception. Pour les exceptions (checked ou non) on encapsule celle-ci dans une Error de type AssertionError. Par contre, les Errors comme par exemple StackOverflowError ou OutOfMemoryError n'ont pas besoin d'être encapsulées dans une AssertionError.
    Modifier le code de la méthode runOnly pour avoir le bon comportement.
    Vérifier que les tests marqués "Q3" passent.

  4. On souhaite maintenant ajouter la méthode ensure qui pourra être chaînée avec equalsTo de telle façon que le code suivant fonctionne
    UnitTest.ensure(42).equalsTo(42); // ne fait rien
    UnitTest.ensure(42).equalsTo(43); // provoque une AssertionError
           
    Contrairement à checkEquals qui permet de tester que deux valeurs sont égales même si elles n'ont pas le même type, on veut que le code qui utilise ensure() et equalsTo avec des types différents ne compile pas.
    Par exemple, le code suivant ne doit pas compiler, car on compare un entier et une chaîne de caractères.
    UnitTest.ensure("42").equalsTo(42);  // ne compile pas
           
    La méthode ensure() renvoie un objet de type Ensure qui est un type interne à UnitTest. Les objets de ce type possèdent une méthode equalsTo ayant le comportement indiqué dans l'exemple ci dessus.
    Écrire le code de la méthode ensure() (sans oublier la méthode equalsTode l'objet qu'elle renvoie).
    Vérifier que les tests marqués "Q4" passent.

  5. Ajouter la méthode not() au type Ensure tel que le code suivant fonctionne.
    UnitTest.ensure(3).not().equalsTo(7);
           
    Quel doit être le type d'objet renvoyé par la méthode not() pour que ce soit possible ?
    Si les deux valeurs sont égales, une AssertionError avec le message "<value1> is equal to <value2>" doit être levée.
    Modifier votre code en conséquence et vérifier que les tests marqués "Q5" passent.

  6. On souhaite maintenant écrire une méthode ensureCode qui prend en argument une fonction qui ne prend pas d'argument et renvoie une valeur (ou lève une exception). Dans ce cas, on veut pouvoir faire des tests sur la valeur de retour en appelant la méthode returnValue() sur le retour de l'appel à ensureCode.
    Voici un exemple d'utilisation
    UnitTest.ensureCode(() -> 42).returnValue().equalsTo(42); // ne fait rien
    UnitTest.ensureCode(() -> 42).returnValue().equalsTo(43); // provoque une AssertionError
           
    Quel doit être le type d'objet renvoyé par la méthode returnValue() pour que ce soit possible ?
    Attention, la fonction fournie en paramètre de la méthode ensureCode pourrait elle-même lever une Exception qui serait un cas d'échec du test.
    Modifier votre code pour avoir le bon comportement.
    Vérifier que les tests marqués "Q6" passent.

  7. On souhaite aussi être capable de vérifier que l'exception levée par une fonction en argument de ensureCode() est du type que l'on souhaite. Pour cela, on le code suivant doit fonctionner (c'est à dire ne pas provoquer d'erreur) :
    UnitTest.ensureCode(() -> { throw null; }).throwsAnException(NullPointerException.class);
           
    Ajouter une méthode throwsAnException à l'objet renvoyé par ensureCode ; elle prend en paramètre la classe de l'exception que l'on souhaite obtenir et vérifie qu'elle est bien lancée par la fonction prise en paramètre par la méthode ensure.
    La méthode doit lever une AssertionError avec le message "expect <exception> but no exception was thrown" si aucune exception n'est levée et "unexpected exception <exception>" si ce n'est pas la bonne exception qui est levée, ou ne rien faire sinon.
    De plus, throwsAnException ne doit pas compiler si l'on passe en argument une classe qui n'est pas une exception. Par exemple le code suivant ne compile pas car String n'est pas une exception.
    UnitTest.ensureCode(() -> 42).throwsAnException(String.class);  // ne compile pas
           
    Écrire le code de throwsAnException puis vérifier que les tests marqués "Q7" passent.

  8. On souhaite écrire le type Report qui stocke une liste de noms de test ainsi qu'une table associative qui associe à un nom de test une liste (qui peut être vide) des Errors qui se sont produites en exécutant les actions du test.
    Écrire le type Report (un type interne de UnitTest) muni des méthodes names() et errors() qui renvoient respectivement la liste des noms et la table associative des erreurs.
    Vérifier que les tests marqués "Q8" passent.

  9. Nous avons presque toutes les pièces du puzzle pour écrire la méthode runAll.
    Avant cela, modifier le code pour permettre qu'un test ait plusieurs actions associées. Puis ajouter la méthode runAll qui exécute les actions de tous les tests. Les actions d'un test doivent s'exécuter dans l'ordre indiqué par l'utilisateur ; par contre, il n'y a pas d'ordre spécifié en terme d'exécution entre les différents tests.
    Écrire le code de runAll puis vérifier que les tests marqués "Q9" passent.
    Remarque : il y a un bonus si les tests (pas les actions de chaque test) s'exécutent en parallèle.