Getting Started with Dropwizard: Testing
Published on:Table of Contents
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 Saying
s 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.
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
If you'd like to leave a comment, please email [email protected]