menu icon

A convenient way to read a resource file in Java unit test

Following the precedent article "A convenient way to read resources in Java", we will learn how to use @InjectResources to inject JUnit 5 and 4 tests with content of resources.

A convenient way to read a resource file in Java unit test

Here at Adelean, we work a lot with Elasticsearch, so it’s not uncommon for us to deal with huge JSON objects. Neither is it surprising that 90% of our tests involves comparison of JSON documents.

In the article “A convenient way to read resources in Java” we saw that reading resources with Java is harder than it should be. We also saw how to simplify this task using library @InjectResources. In this article we will see how to inject our JUnit 5/4 tests with content of resources.

Introduction

@InjectResources comes with extensions for popular test frameworks JUnit5 and JUnit4.

Let’s start by adding necessary dependencies to our project:

With 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'
}

Or with 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>

Let’s assume that we have a resource file /com/adelean/junit/jupiter/resource.txt containing this text:

The quick brown fox jumps over the lazy dog.

To inject this text on our JUnit5 tests we need:

@TestWithResources  // <-- add extension
public class InjectTextResourcesTests {

    @GivenTextResource("/com/adelean/junit/jupiter/resource.txt")
    String resourceContent;   // <-- inject resource content

    @Test
    public void testInjectTextIntoStringInstanceField() {
        assertThat(resourceContent)
                .isEqualTo("The quick brown fox jumps over the lazy dog.");
    }
}
  1. Add extension using annotation @TestWithResources
  2. Request the content of resource with @GivenTextResource

After that, the content of resource resource.txt appears inside the field resourceContent and can be used in tests.

JUnit4 on the other side uses Rules. The same operation with JUnit4 can be done as following:

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.");
    }
}

It’s recommended to create resource rules using factory method com.adelean.inject.ressources.junit.vintage.GivenResource.givenResource imported statically. Following examples will omit that import.

Binary resources

To import binary resources we can use @GivenBinaryResource annotation for JUnit5 or BinaryResource rule for JUnit4.

With 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);
    }
}

With 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);
    }
}

Java properties

In a similar way, we can use annotation @GivenPropertiesResource or PropertiesResource rule to read Java properties.

With 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");
    }
}

With 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");
    }
}

We saw how the library handles simple formats. But it can do much more. We start the most interesting part of this article because @InjectResources considerably simplifies loading and parsing of JSON and YAML.

JSON and JSON lines

To parse JSON library natively supports Jackson and GSON.

Let’s see how it works with 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;
}

In order for this snippet to work, jackson-core and jackson-databind must be present on the classpath. JSON parsing requires declaration of ObjectMapper annotated with @WithJacksonMapper. After, all we need is to add annotation @GivenJsonResource on the field of type Map, JsonNode or any POJO to get parsed content of JSON, or annotation @GivenJsonLinesResource for JSON lines.

Parsing with GSON works in similar manner. The official documentation gives us an example: https://hosuaby.github.io/inject-resources/0.1.0/asciidoc/#_gson

For Junit4 we have rules JsonResource and JsonLinesResource. The difference with JUnit5 annotations is that we need to supply ObjectMapper or Gson object to rules builder.

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

Parsing of YAML is similar to JSONs. For this purpose @InjectResources uses Snakeyaml that must be present on the classpath.

For JUnit5 use annotations @GivenYamlResource and @GivenYamlDocumentsResource (YAML documents is a format of a file that contains multiple YAML documents separated by three hyphens ---).

@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;
}

For JUnit4 use rules YamlResource and 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

We finished this presentation of @InjectResources for JUnit. This tool can save you a lot of time if you work with resources a lot.

Do not hesitate to take a look into official documentation: