Pages

mercredi 19 juin 2013

Les asserts dans vos tests unitaires

Suite au ligtning talk fait au Jug Lyon sur ce sujet (voir les slides), je reviens sur la manière de faire des asserts dans vos tests unitaires en Java.


Junit et les asserts
Je ne vais pas présenter Junit que tout développeur est sensé connaître mais je vais plus m’attarder sur les assertions que l’on fait à la suite de l’exécution des tests. Junit 4 propose deux classes pour vérifier les insertions pour tous les types et objets primitifs
  • junit.framework.Assert
  • org.junit.Assert
C’est une des incohérences de cette merveilleuse API qui a su s'imposer comme un standard. Après l’arrivée de Java 5 et des generics, Junit a été réécrit en partie mais au lieu de faire vivre deux versions du projet en parallèle les créateurs ont préféré avoir un seul projet assurant sa propre rétrocompatibilité, même si des classes sont dupliquées dans les deux packages. Ce devrait être résolu prochainement puisqu’une étape a été franchie depuis la version 4.11. En effet les classes en doublon sont passées en @Deprecated dans junit.framework.

La page du projet est https://github.com/junit-team/junit/wiki

Pour récupérer Junit dans un repo Maven voici la description de l’artefact

       <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
        </dependency>

Revenons aux assertions, le principe est de faire échouer la méthode de test si l’assertion n’est pas vérfiée. En Junit on aura

// JUnit
assertEquals(Object expected, Object actual)
assertSame(Object expected, Object actual)
assertNotSame(Object expected, Object actual)
assertNull(Object object)
assertNotNull(Object object)
assertTrue(boolean condition)
assertFalse(boolean condition)
fail(String)

Ces méthodes s’appliquent pour tous les types primitifs et vous pouvez ajouter une chaine de caractères pour spécifier le message qui sera retourné


Dans la version 4 (org.junit.Assert) des méthode pour comparer des tableaux ont été ajoutées
assertArrayEquals(Object[] expecteds, Object[] actuals)

Un exemple
        Framework fwk = classeATester.getById(1L);
        Assert.assertEquals(Long.valueOf(1), fwk.getId());
        Assert.assertEquals(ProgrammingLanguage.JAVA, fwk.getLanguage());
        Assert.assertEquals("Junit", fwk.getName());
        Assert.assertEquals("4.11", fwk.getVersion());

Mettons de côté la technique et regardons comment on traduit en français ce type de message


  • Est ce que égaux sont les objet attendu et objet actuel ?
  • Est ce que null est mon objet ?
  • Est ce que true est ma condition?

Et non ce n’est pas un remake de Star Wars mais bien les assertions Junit

Hamcrest
Joe Walnes lors de la mise en place du framework JMock a mis au point un nouveau mécanisme d’assertion qui a donné lieu au projet Hamcrest (http://hamcrest.org/).

Hamcrest a été intégré à Junit à partir de la version 4.4 dans sa version 1.2 et la version 1.3 a été intégré à partir de Junit 4.11

Hamcrest propose une nouvelle méthode pour vérifier les assertions
assertThat([value], [matcher statement]);

Cette syntaxe est déjà plus lisible. Si je veux verifier que i=5

  • En Junit on avait “Est ce que Egal à 5 est i ?”
  • Avec Hamcrest on aura “Est ce que i est égal à 5 ?”

Un avantage supplémentaire d’Hamcrest est d’amener un grand nombre de matchers et de vous proposer une manière simple pour créer les votre. Une partie des Matchers a été intégrée à Junit dans org.hamcrest.CoreMatchers et org.junit.matchers.JUnitMatchers

En utilisant des imports statiques des classes, on commence à avoir des tests plus lisibles

        Framework fwk = classeATester.getById(1L);
        assertThat(fwk.getId(), equalTo(1L));
        assertThat(fwk.getLanguage(), equalTo(ProgrammingLanguage.JAVA));
        assertThat(fwk.getName(), equalTo("Junit"));
        assertThat(fwk.getVersion(), equalTo("4.11"));

Mais la syntaxe pourrait être encore plus claire. C’est là que rentre en jeu Fest-Assert qui propose une fluent API et un très grand nombre de matcher.

Fest Assert
Le projet Fest http://fest.easytesting.org/ est découpé en plusieurs modules offrant des fonctionnalités pour simplifier la mise en place de vos tests. Fest-swing permet par exemple d’écrire des tests fonctionnels sur des applications écrites en Swing. Nous nous intéresserons qu’au module facilitant l’écriture des assertions, fest-assert.

Pour récupérer Fest-assert dans un repo Maven voici la description de l’artefact
        <dependency>
            <groupId>org.easytesting</groupId>
            <artifactId>fest-assert</artifactId>
            <version>1.4</version>
        </dependency>

Au niveau de la syntaxe si je reprends l’exemple précédent nous aurons

        Framework fwk = classeATester.getById(1L);
        assertThat(fwk.getId()).isEqualTo(1L);
        assertThat(fwk.getLanguage()).isNotNull().isIn(ProgrammingLanguage.values())
                .isEqualTo(ProgrammingLanguage.JAVA);
        assertThat(fwk.getName()).hasSize(5).isEqualTo("Junit");
        assertThat(fwk.getVersion()).startsWith("4.").isEqualTo("4.11");


Avec Fest-assert une assertion se résume en une phrase composée d’un sujet, verbe et complément. Par exemple : Est ce que le nom a une taille de 5 caractères et est ce qu’il est égal à “Junit” ?

Un des avantages de fest-assert est de proposer des méthodes d’assertions différentes suivant la nature de l’objet. On trouve des asserts pour ne nombreux objets Boolean, Byte, Char, Double, Float, Integer, Long, Short, String, Throwable, File, Array, List, Image, Iterator.

Pour les listes on arrive à avoir des choses assez évoluées comme par exemple
List<Framework> fwks = classeATester.getAll();
        assertThat(fwks).isNotEmpty().hasSize(3)
                .onProperty("name").containsExactly("Junit","TestNG","Hamcrest");

Vous pouvez bien sûr créer vos propres méthodes d’assertion. Par exemple

/**
 * Objet permettant de faciliter le controle d'un objet Framework
 */
public class FrameworkAssert extends GenericAssert<FrameworkAssert, Framework> {

    /**
     * Creates a new <code>{@link org.fest.assertions.GenericAssert}</code>.
     * @param actual   the actual value to verify.
     */
    protected FrameworkAssert(Framework actual) {
        super(FrameworkAssert.class, actual);
    }


    /**
     * an entry point for FrameworkAssert
     */
    public static FrameworkAssert assertThat(Framework actual) {
        return new FrameworkAssert(actual);
    }


    /**
     * Vérifie que les champs de la clé fonctionnelle sont renseignés
     */
    public FrameworkAssert hasUniqueKey() {
        Assertions.assertThat(actual.getName()).overridingErrorMessage("Name is required").isNotNull();
        Assertions.assertThat(actual.getVersion()).overridingErrorMessage("Version is required").isNotNull();
        Assertions.assertThat(actual.getLanguage()).overridingErrorMessage("Language is required").isNotNull();
        return this;
   }
}

Conclusion
Au final si on compare les trois, on peut voir que la syntaxe la plus claire est celle de fest-assert

// JUnit
assertEquals(expected, actual);

// Hamcrest
assertThat(actual, equalTo(expected));

// FEST-Assert
assertThat(actual).isEqualTo(expected);

Vous pouvez retrouver un projet exemple sur Github à l’adresse suivante https://github.com/javamind/annexe/tree/master/JugLyonFestAssert

Il est important d’écrire des classes de tests les plus simples possible pour faciliter leur lecture. N’hésitez pas à utiliser des commentaires et des noms explicites dans le nom de vos méthodes de tests

/**
 * Test de la classe {@link com.ehret.jug.facade.FrameworkFacade}
 */
public class AssertFestJunitTest {
    private IFrameworkFacade classeATester;


    @Before
    public void init(){
        classeATester = new FrameworkFacade();
    }
    

    /**
     * test de {@link com.ehret.jug.facade.IFrameworkFacade#getById(Long)} avec argument vide
     */
    @Test
    public void getByIdWithIdNullShouldReturnAnException(){
        try{
            classeATester.getById(null);
            Assert.fail("Doit planter avant");
        }
        catch (RuntimeException e){
            assertThat(e).isInstanceOf(NullPointerException.class).hasMessage("Framework id is required");
        }
    }


    /**
     * test du cas nominal {@link com.ehret.jug.facade.IFrameworkFacade#getById(Long)}
     */
    @Test
    public void getByIdShouldReturnAFramework(){
        Framework fwk = classeATester.getById(1L);
        assertThat(fwk.getId()).isEqualTo(1L);
        assertThat(fwk.getLanguage()).isNotNull().isIn(ProgrammingLanguage.values())
                .isEqualTo(ProgrammingLanguage.JAVA);
        assertThat(fwk.getName()).hasSize(5).isEqualTo("Junit");
        assertThat(fwk.getVersion()).startsWith("4.").isEqualTo("4.11");

        FrameworkAssert.assertThat(fwk).hasUniqueKey();
    }

}

Aucun commentaire:

Enregistrer un commentaire

Remarque : Seul un membre de ce blog est autorisé à enregistrer un commentaire.