menu icon

Injecter les ressources dans les tests JUnit

La suite de l'article "Lire et parser les ressources en Java facilement". Nous allons voir comment utiliser les extensions de @InjectResources pour les frameworks de tests JUnit 5 et 4.

Injecter les ressources dans les tests JUnit

Chez Adelean, on travaille beaucoup avec Elasticsearch, et par conséquent on a l’habitude de manipuler des gros objets JSON. Et ce n’est pas surprenant que 90% de nos tests consistent à comparer les documents JSON.

Dans l’article “Lire et parser les ressources en Java facilement”, nous avons vu pourquoi lire les fichiers ressources en Java est plus compliqué que ça ne devrait être. Nous avons vu également comment simplifier cette tâche avec la librairie @InjectResources. Dans cet article, nous verrons comment injecter le contenu de fichiers ressources dans nos tests JUnit (4 et 5).

@InjectResources fournit les extensions pour les frameworks de tests JUnit5 et JUnit4.

Commonçons par les ajouter en tant que dépendances:

Avec Gradle:

repositories {
    jcenter()
}

dependencies {
    testCompile group: 'com.adelean', name: 'inject-resources-core', version: '0.1.0'

    // For JUnit5
    testCompile group: 'com.adelean', name: 'inject-resources-junit-jupiter', version: '0.1.0'

    // or for JUnit4
    testCompile group: 'com.adelean', name: 'inject-resources-junit-vintage', version: '0.1.0'
}

Ou avec Maven:

<repositories>
    <repository>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
        <id>central</id>
        <name>bintray</name>
        <url>https://jcenter.bintray.com</url>
    </repository>
</repositories>

<dependencies>
    <dependency>
        <groupId>com.adelean</groupId>
        <artifactId>inject-resources-core</artifactId>
        <version>0.1.0</version>
        <scope>test</scope>
    </dependency>

    <!-- For JUnit5 -->
    <dependency>
        <groupId>com.adelean</groupId>
        <artifactId>inject-resources-junit-jupiter</artifactId>
        <version>0.1.0</version>
        <scope>test</scope>
    </dependency>

    <!-- or for JUnit4 -->
    <dependency>
        <groupId>com.adelean</groupId>
        <artifactId>inject-resources-junit-vintage</artifactId>
        <version>0.1.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Supposons que nous avons un fichier ressource qui s’appelle /com/adelean/junit/jupiter/resource.txt et qui contient ce texte:

The quick brown fox jumps over the lazy dog.

Pour utiliser son contenu dans nos tests JUnit5 il faut:

@TestWithResources  // <-- ajouter l'extension
public class InjectTextResourcesTests {

    @GivenTextResource("/com/adelean/junit/jupiter/resource.txt")
    String resourceContent;   // <-- injecter le contenu de la ressource

    @Test
    public void testInjectTextIntoStringInstanceField() {
        assertThat(resourceContent)
                .isEqualTo("The quick brown fox jumps over the lazy dog.");
    }
}
  1. Ajouter l’extension avec l’annotation @TestWithResources
  2. Demander le contenu de la ressource avec @GivenTextResource

Après cela, le contenu de resource.txt apparait dans le champ resourceContent et peut être utilisé dans les tests.

JUnit4 de son côté utilise le mécanisme des Rules. La même opération avec JUnit4 peut être faite de façon suivante:

import static com.adelean.inject.resources.junit.vintage.GivenResource.givenResource;

class MyTestClass {

    @Rule
    public ResourceRule<String> textResource = givenResource()
            .text("/com/adelean/junit/jupiter/resource.txt")
            .withCharset(StandardCharsets.UTF_8);

    @Test
    public void testWithTextResource() {
        assertThat(textResource.get())
                .isEqualTo("The quick brown fox jumps over the lazy dog.");
    }
}

La méthode privilégiée pour créer les Rules de ressources c’est d’utiliser la méthode factory com.adelean.inject.ressources.junit.vintage.GivenResource.givenResource. Il est recommandé d’importer cette méthode statiquement. Les exemples suivants vont omettre cet import.

Ressources binaires

Pour importer les ressources binaires, on peut utiliser l’annotation @GivenBinaryResource pour JUnit5 ou le rule BinaryResource pour JUnit4.

Example JUnit5:

@TestWithResources
class InjectBinaryResourcesTests {

    @GivenBinaryResource("/com/adelean/junit/jupiter/fibonacci.bin")
    byte[] fibonacci;

    @Test
    public void testInjectBinaryContentIntoByteArrayInstanceField() {
        assertThat(fibonacci)
                .contains(1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89);
    }
}

Example JUnit4:

class MyTestClass {

    @Rule
    public BinaryResource fibonacci = givenResource()
            .binary("/com/adelean/junit/jupiter/fibonacci.bin");

    @Test
    public void testWithBinaryResource() {
        assertThat(fibonacci.get())
                .contains(1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89);
    }
}

Properties JAVA

De la même manière, on peut utiliser l’annotation @GivenPropertiesResource ou le rule PropertiesResource pour lire les properties Java.

Example JUnit5:

@TestWithResources
class InjectPropertiesResourcesTests {

    @GivenPropertiesResource("/com/adelean/junit/jupiter/db.properties")
    Properties dbProperties;

    @Test
    public void testInjectTextIntoStringInstanceField() {
        assertThat(dbProperties)
                .containsEntry("db.user", "hosuaby")
                .containsEntry("db.password", "password")
                .containsEntry("db.url", "localhost");
    }
}

Example JUnit4:

class MyTestClass {

    @Rule
    public PropertiesResource dbProperties = givenResource()
            .properties("/com/adelean/junit/jupiter/db.properties");

    @Test
    public void testWithJavaPropertiesResource() {
        assertThat(dbProperties.get())
                .containsEntry("db.user", "hosuaby")
                .containsEntry("db.password", "password")
                .containsEntry("db.url", "localhost");
    }
}

On a vu comment la librairie gère les formats simples. Mais elle ne se limite pas à ça. On aborde la partie la plus intéressante, car @InjectResources simplifie beaucoup le chargement et la lecture des ressources en formats JSON et YAML.

JSON et JSON lines

Pour lire le JSON, la librairie supporte nativement Jackson et GSON.

Regardons comment ça marche avec Jackson:

@TestWithResources
public class TestsWithJackson {

    @WithJacksonMapper
    ObjectMapper objectMapper = new ObjectMapper()
            .registerModule(new JavaTimeModule());

    /* JSON */
    @GivenJsonResource("/com/adelean/junit/jupiter/sponge-bob.json")
    Map<String, Object> jsonAsMap;

    @GivenJsonResource("/com/adelean/junit/jupiter/sponge-bob.json")
    JsonNode jsonNode;

    @GivenJsonResource("/com/adelean/junit/jupiter/sponge-bob.json")
    Person spongeBob;

    /* JSONL */
    @GivenJsonLinesResource("/com/adelean/junit/jupiter/logs.jsonl")
    Log[] logsAsArray;

    @GivenJsonLinesResource("/com/adelean/junit/jupiter/logs.jsonl")
    Collection<Log> logsAsCollection;
}

Pour que ce morceau de code marche, il faut que jackson-core et jackson-databind soient présents sur le classpath. La lecture des ressources JSON nécessite la déclaration de ObjectMapper annoté avec @WithJacksonMapper. Ensuite il suffit d’ajouter l’annotation @GivenJsonResource pour parser le contenu en tant qu’une Map, JsonNode ou un POJO, ou l’annotation @GivenJsonLinesResource pour lire le contenu en format JSON lines.

La lecture avec le GSON marche de la même manière. La documentation officielle en fournit un exemple: https://hosuaby.github.io/inject-resources/0.1.0/asciidoc/#_gson

Pour Junit4 nous avons les rules JsonResource et JsonLinesResource. A la différence des annotations de JUnit5 l’objet ObjectMapper ou Gson doit être passé dans le builder de Rule.

class MyTestClass {

    /* Parse JSON with Jackson */
    @Rule
    public JsonResource<Map<String, Object>> jsonAsMap = givenResource()
            .json("/com/adelean/junit/jupiter/sponge-bob.json")
            .parseWith(new ObjectMapper());

    /* Parse JSON lines with Gson */
    @Rule
    public JsonLinesResource<Collection<Log>> logsAsCollection = givenResource()
            .jsonLines("/com/adelean/junit/jupiter/logs.jsonl")
            .parseWith(new Gson());
}

YAML

Le chargement de YAML ressemble à celui de JSON. @InjectResources utilise la librairie Snakeyaml qui doit être présente sur le classpath.

Pour JUnit5 utilisez les annotations @GivenYamlResource et @GivenYamlDocumentsResource (YAML documents c’est un format du fichier qui contient plusieurs documents YAML dans le même fichier séparés par trois tirets ---).

@TestWithResources
class TestsWithYaml {

    @WithSnakeYaml
    Yaml yaml = new Yaml();

    /* YAML resource with a single document */
    @GivenYamlResource("/com/adelean/junit/jupiter/receipt.yml")
    Map<String, Object> receipt;

    @GivenYamlResource("/com/adelean/junit/jupiter/sponge-bob.yaml")
    Person spongeBob;

    /* YAML resource with multiple documents separated by '---' */
    @GivenYamlDocumentsResource("/com/adelean/junit/jupiter/stacktrace.yaml")
    List<Map<String, Object>> stacktraceAsList;
}

Pour JUnit4 il faut utiliser les rules YamlResource ou YamlDocumentsResource.

class MyTestClass {

    /* Load and parse YAML resource */
    @Rule
    public YamlResource<Person> spongeBob = givenResource()
            .yaml("/com/adelean/junit/jupiter/sponge-bob.yaml")
            .parseWith(new Yaml());

    /* Load and parse YAML documents resource */
    @Rule
    public YamlDocumentsResource<Log[]> logsAsArray = givenResource()
            .yamlDocuments("/com/adelean/junit/jupiter/logs.yml")
            .parseWith(new Yaml(new Constructor(Log.class)));
}

Conclusion

Nous avons terminé la présentation de @InjectResources pour JUnit. Cette librairie, même si elle ne révolutionnera pas votre quotidien, vous économisera les précieuses minutes si vous manipulez souvent les ressources.

N’hésitez pas à aller voir la documentation officielle: