11 nov. 2015

JUnit paramétriques

L'importante de la qualité des tests unitaires dans les projets logiciels est parfois négligée. En effet, écrire des tests unitaires en Java se révèle être parfois une corvée, voire une tâche subalterne que l'on réserve aux stagiaires. Cependant, les JUnits sont un facteur clé de :
  • Non régression de l'application
  • Conformité des règles fonctionnelles définie avec le client



Les JUnits classiques consistent à tester une classe avec un JUnit (généralement une classe de test portant même nom, suffixée par "Test"). Pour chaque cas à tester, on crée une méthode de tests. Il y a donc 2 cas de figure :
  • Il y a donc autant de méthodes de tests que tests à effectuer, toute méthode confondue
  • Il y a une méthode de tests par méthode à tester, en testant tous les cas voulus de la méthode à tester en une seule fois. La méthode de test devient alors conséquente et surtout, ne tire pas profit des atouts de la librairie JUnit (ex : possibilité de réinitialiser le test via @Before
Ex de test classique :

Classe à tester :
 


public class ClassATester {
  /**
   * Retourne une valeur définie en paramètre si le premier paramètre est null.
   *
   * @param o a remplacer par la valeur non nulle.
   * @param valueIfNull la valeur a renvoyer si o est null.
   * @return valueIfNull si o est null.
   */
  public static Object defaultIfNull(Object o, Object valueIfNull) {
    return o == null ? valueIfNull : o;
  }
}



Classe de test



public class ClassATesterTest {
  /**
   * Test de defaultIfNull avec un paramètre non null de type Double.
   */
  @Test
  public static Object testDefaultIfNullAvecNonNull() {
    Object o = ClassATester.defaultIfNull(10d, 0d);
    Assert.assertNotNull(o);
    Assert.assertEquals(o, 10d);
  }
  
  /**
   * Test de defaultIfNull avec un paramètre null de type Double.
   */
  @Test
  public static Object testDefaultIfNullAvecNonNull() {
    Object o = ClassATester.defaultIfNull(null, 0d);
    Assert.assertNotNull(o);
    Assert.assertEquals(o, 0d);
  }

  // On peut rajouter les tests suivants :
  // Test avec d'autres types que Double, en vérifiant le cas non null et null
  
}



Tout ce discours ne contribue pas à rendre plus attrayant les tests :p. Cependant, il existe des librairies permettant de faire les JUnits autrement. La librairie présentée ici sera JUnitParams, qui permet de faire des tests paramétriques.

Le principe de JUnitParams est pouvoir passer des jeux de paramètres à une méthode, ce qui amène à la proposition suivante :

A chaque méthode à tester sera associé une seule méthode de test qui sera jouée autant de fois qu'il y a de jeu de paramètres. 

Ce postulat permet donc de définir :
  • Une classe de test pour chaque classe à tester
  • Une méthode de test, pour chaque méthode à tester. Chaque méthode de test sera paramétrée pour jouer différents cas de test. On évite ainsi de créer n méthodes test pour tester une seule méthode
Pour ce faire, il faut indiquer que le runner de test que JUnit doit utiliser est celui de JUnitParams.Pour ce faire, il suffit d'annoter la classe de test avec

@RunWith(JUnitParamsRunner.class)

Pour créer des méthodes paramétrées, JUnitParams met à disposition des exemples.
Le principe est de passer les différents paramètres de test dans un Iterable, en le spécifiant en annotant la méthode de test avec :

@Parameters(source= uneSourceIterable)

On peut définir toutes sortes de jeux de paramètres via des enums, des listes, des méthode qui renvoient un Iterable, etc.

Dans le cas d'une source définie par une Classe / Enum, le jeu de paramètres doit être fournit par une méthode préfixée par "provide" (ex : provideParameters), définie dans la classe définie comme source de paramètres via l'annotation @Parameters.

Voici un exemple de source de paramètres définie via un Enum



// Doit être public
public enum ParametresTestObjectUtils {
    /**
     * Test avec un double à 10.
     */
    TEST_DOUBLE_10(10d, 0d, 10d),
    
    /**
     * Test avec un double avec une valeur max.
     */
    TEST_DOUBLE_MAX(Double.MAX_VALUE, 0d, Double.MAX_VALUE),

    /**
     * Test avec un double null remplacé par 0d.
     */
    TEST_DOUBLE_NULL_DEFAULT_0(null, 0d, 0d),

    /**
     * Test avec un String à "test".
     */
    TEST_STRING_NON_NULL("test", "", "test"),

    /**
     * Test avec un String null remplacé "".
     */
    TEST_STRING_NULL(null, "", "");
    // D'autres cas de test.

    /**
     * La valeur à remplacer si null.
     */
    private Object o;
    
    /**
     * La valeur de remplacement si o est null.
     */
    private Object defaultIfNull;

    /**
     * La valeur attendue apres appel de la méthode defaultIfNull.
     */
    private Object valeurAttendue;

    /**
     * Définit un cas de test de defaultIfNull.
     */
    public enum ParametresTestObjectUtils(Object o, Object defaultIfNull, Object valeurAttendue) {
        this.o = o;
        this.defaultIfNull = defaultIfNull;
        this.valeurAttendue = valeurAttendue;
    }

    // La méthode qui fournit les cas de test est :
    // - publique
    // - préfixée par "provide"
    // retourne une Iterable du type qui sera passé en paramètre à la méthode de test
    // On aura donc une méthode de test qui aura cette signature :
    // 
    // @Test
    // @Parameters(source = ParametresTestObjectUtils.class) -> Source des jeux de test
    // public void testDefaultIfNull(ParametresTestObjectUtils test) -> La méthode sera invoquée autant de fois qu'il y a de paramètres fournis par ParametresTestObjectUtils  
    /**
     * @return les cas de tests de defaultIfNull.
     */
    public static ParametresTestObjectUtils[] provideParameters() {
        return values();
    }
}
 
Dans les coulisses voici ce qui se passe :

  1. JUnitParams va créer une fois pour toute les instances de l'enum.
  2. JUnitParams va appeler la méthode de test (annotée @Test) associée au jeu de test (annotée @Parameters(source = ParametresTestObjectUtils.class)) autant de fois qu'il y a de cas de tests fournis par l'enum ParametresTestObjectUtils, via la méthode provideParameters, soit 5 fois. A chaque appel, la méthode de test sera invoquée avec en paramètre le cas de test courant de type ParametresTestObjectUtils.

Exemple de méthode de test utilisant le jeu de paramètres ParametresTestObjectUtils :


// testObjectIfNull sera appelée autant de fois qu'il y a de cas de tests fournis par ParametresTestObjectUtils
@Test
@Parameters(source = ParametresTestObjectUtils.class)
public void testObjectIfNull(ParametresTestObjectUtils test) {
    Object res = ClassATester.objectIfNull(test.o, test.defaultIfNull);
    Assert.assertEquals(test.valeurAttendue, res);
}

JUnitParams est donc un outil puissant qui permet de mieux organiser les tests Unitaires.