Getting Started with Dropwizard: Testing

Some Dropwizard code coverage

The Dropwizard Getting Started guide is a great introduction to the framework, but it in regards to testing the created applications it leaves some wanting. This post will walk one through adding logical and practical tests to the application created in guide and in the end, we will achieve with 100% test coverage.

Baby Steps

Add the dropwizard-testing to your pom.xml. It will pull in common libraries used testing such as JUnit and AssertJ that we’ll use immediately, as well as additional resources for more full stack testing.

<dependency>
    <groupId>io.dropwizard</groupId>
    <artifactId>dropwizard-testing</artifactId>
    <version>${dropwizard.version}</version>
</dependency>

Before we get into the web domain, let’s ensure that the endpoint we created in HelloWorldResource operates as we expect. The source code below should be mostly self-explanatory, as we are dealing with just our class. The annotations such as Path, GET, Produces, etc, that appear in our resource class do nothing if there is no one to interpret them. There is one potential pitfall, which I’ll go into more detail about after the code.

package com.example.helloworld;

import com.example.helloworld.core.Saying;
import com.example.helloworld.resources.HelloWorldResource;
import com.google.common.base.Optional;
import org.junit.Before;
import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class HelloWorldResourceTest {
    private HelloWorldResource resource;

    @Before
    public void setup() {
        // Before each test, we re-instantiate our resource so it will reset
        // the counter. It is good practice when dealing with a class that
        // contains mutable data to reset it so tests can be ran independently
        // of each other.
        resource = new HelloWorldResource("Hello, %s", "Stranger");
    }

    @Test
    public void idStartsAtOne() {
        Saying result = resource.sayHello(Optional.of("dropwizard"));
        assertThat(result.getId()).isEqualTo(1);
    }

    @Test
    public void idIncrementsByOne() {
        Saying result = resource.sayHello(Optional.of("dropwizard"));
        Saying result2 = resource.sayHello(Optional.of("dropwizard2"));

        assertThat(result2.getId()).isEqualTo(result.getId() + 1);
    }

    @Test
    public void absentNameReturnsDefaultName() {
        Saying result = resource.sayHello(Optional.<String>absent());
        assertThat(result.getContent()).contains("Stranger");
    }

    @Test
    public void nameReturnsName() {
        Saying result = resource.sayHello(Optional.of("dropwizard"));
        assertThat(result.getContent()).contains("dropwizard");
    }
}

The one thing of note is the Before attribute and as the comment explains, its job is to run once before each test to instantiate our resource. We do this to emphasize that internal state is managed by our resource (the counter). We could have omitted the setup function and used the same resource for all tests like the following:

private HelloWorldResource resource =
    new HelloWorldResource("Hello, %s", "Stranger");

And all the tests will work together and independently – only because we got lucky. If idIncrementsByOne executed before idStartsAtOne, then the check of isEqualTo(1) would fail because the counter was already incremented by the previous test case.

Endpoint Testing

Now we are confident our resource behaves correctly. Let’s write tests to ensure that clients are receiving the JSON we think we are returning. To achieve this, we will need some help from Dropwizard to setup our class appropriately and host an in-memory server for us to query.

I’ll post the snippet of code and then dive deeper.

package com.example.helloworld;

import com.example.helloworld.core.Saying;
import com.example.helloworld.resources.HelloWorldResource;
import com.fasterxml.jackson.databind.ObjectReader;
import io.dropwizard.testing.junit.ResourceTestRule;
import org.junit.Rule;
import org.junit.Test;

import java.io.IOException;

import static org.assertj.core.api.Assertions.assertThat;

public class HelloWorldEndpointTest {

    @Rule
    public final ResourceTestRule resources = ResourceTestRule.builder()
            .addResource(new HelloWorldResource("Hello, %s!", "Stranger"))
            .build();

    @Test
    public void helloWorldDropwizard() throws IOException {
        // Hit the endpoint and get the raw json string
        String resp = resources.client().target("/hello-world")
                .queryParam("name", "dropwizard")
                .request().get(String.class);

        // The expected json that will be returned
        String json = "{ \"id\": 1, \"content\": \"Hello, dropwizard!\" }";

        // The object responsible for translating json to our class
        ObjectReader reader = resources.getObjectMapper().reader(Saying.class);

        // Deserialize our actual and expected responses
        Saying actual = reader.readValue(resp);
        Saying expected = reader.readValue(json);

        assertThat(actual.getId())
                .isEqualTo(expected.getId())
                .isEqualTo(1);

        assertThat(actual.getContent())
                .isEqualTo(expected.getContent())
                .isEqualTo("Hello, dropwizard!");
    }

    @Test
    public void helloWorldAbsentName() {
        // A more terse way to test just an endpoint
        Saying actual = resources.client().target("/hello-world")
                .request().get(Saying.class);
        Saying expected = new Saying(1, "Hello, Stranger!");
        assertThat(actual.getId()).isEqualTo(expected.getId());
        assertThat(actual.getContent()).isEqualTo(expected.getContent());
    }
}

This is quite a chunk of code, but we can break it down. First, note the @Rule. You can imagine that it starts a server with our specified resource for each test case. This means that the resource is recreated for each test case, which is great for us because of the mutable state in our resource. If a resource is immutable, feel free to mark the @Rule as @ClassRule and mark the variable as static – you’ll save a few milliseconds per test.

The first test stresses the entire resource soup to nuts. It simulates a client sending a request to /hello-world?name=dropwizard and we’re saying that the first request will be in the form of:

{
  "id": 1,
  "content": "Hello, dropwizard!"
}

We then map the raw strings into Saying objects by using a Dropwizard configured ObjectMapper that will map the JSON string into the specified class. Finally, we assert that the two Sayings are equivalent and that they are both equal to the constants specified in the JSON string. The final assert against the constants are important because it serves as a sanity check that deserialization (the process of converting a string into an instantiated class) works as designed.

Alternatively, we could have compared the raw JSON strings directly, but that wouldn’t have tested the deserialization code, and we would have to watch out for whitespace differences. If you want to compare JSON strings directly without worrying about whitespace, formatting, and other options (such as null means the same as nonexistent), take a look at JSONassert and JsonUnit – both are high quality libraries.

The second test case, helloWorldAbasentName, shows a more terse way of retrieving a Saying with automatic deserialization and proves that our resource is recreated with each test.

Integration Testing

Our tests don’t stress our configuration class (HelloWorldConfiguration), nor the application class (HelloWorldApplication). Let’s fix that! Usually, integration testing is not as easy as what will be shown because, as the name implies, it integrates all parts of the application (ie. the database). You can’t mock a database in an integration test! It may seem like there an exorbitant amount of work that goes into integration testing, but the effort is well worth it because then you can be self-assured that all the parts are working cohesively together. After integration testing, it should only be a small step to production.

package com.example.helloworld;

import com.example.helloworld.core.Saying;
import io.dropwizard.testing.ResourceHelpers;
import io.dropwizard.testing.junit.DropwizardAppRule;
import org.glassfish.jersey.client.JerseyClientBuilder;
import org.junit.Rule;
import org.junit.Test;

import javax.ws.rs.client.Client;

import static org.assertj.core.api.Assertions.assertThat;

public class HelloWorldIntegrationTest {
    @Rule
    public final DropwizardAppRule<HelloWorldConfiguration> RULE =
        new DropwizardAppRule<HelloWorldConfiguration>(HelloWorldApplication.class,
            ResourceHelpers.resourceFilePath("my-app-config.yaml"));

    @Test
    public void runServerTest() {
        Client client = new JerseyClientBuilder().build();
        Saying result = client.target(
            String.format("http://localhost:%d/hello-world", RULE.getLocalPort())
        ).queryParam("name", "dropwizard").request().get(Saying.class);
        assertThat(result.getContent()).isEqualTo("Hello, dropwizard!");
    }
}

Here we start a new server for each test by instantiating our HelloWorldApplication with the HelloWorldConfiguration that is parsed from the file my-app-config-yaml. The configuration is loaded from test\resources\my-app-config.yaml. The server created for the tests is equivalent to:

java -jar target/hello-world-0.0.1-SNAPSHOT.jar server my-app-config.yaml

After running tests and the code coverage, you should see that there is 100% code coverage outside the main function. Congratulations on achieving a test coverage people dream about!

One caveat, the integration test we created will run whenever our unit tests are ran, which is fine in our case because no external resources are used. In reality, you won’t want to run integration tests with other tests. Splitting tests up is a little outside of scope of this tutorial, but there are satisfactory guides already out there.

As a reference, here is the directory structure that you should have when you finish the tutorial. There are some IDE specific files, such as .idea and dropwizard-example.iml that you can safely ignore.

The final directory structure

Advanced

The in memory server in the endpoint testing covered our resource’s implementation; however, it is not always the case. When the resource uses a @Context or @Auth in an endpoint, you will need an actual test server that is one-step below an integration test, which the Dropwizard documentation goes in more depth about.

Comments: