The dropwizard-testing
module provides you with some handy classes for testing
your representation classes
and resource classes. It also provides a JUnit rule
for full-stack testing of your entire app.
While Jackson’s JSON support is powerful and fairly easy-to-use, you shouldn’t just rely on eyeballing your representation classes to ensure you’re producing the API you think you are. By using the helper methods in FixtureHelpers, you can add unit tests for serializing and deserializing your representation classes to and from JSON.
Let’s assume we have a Person
class which your API uses as both a request entity (e.g., when
writing via a PUT
request) and a response entity (e.g., when reading via a GET
request):
public class Person {
private String name;
private String email;
private Person() {
// Jackson deserialization
}
public Person(String name, String email) {
this.name = name;
this.email = email;
}
@JsonProperty
public String getName() {
return name;
}
@JsonProperty
public void setName(String name) {
this.name = name;
}
@JsonProperty
public String getEmail() {
return email;
}
@JsonProperty
public void setEmail(String email) {
this.email = email;
}
// hashCode
// equals
// toString etc.
}
First, write out the exact JSON representation of a Person
in the
src/test/resources/fixtures
directory of your Dropwizard project as person.json
:
{
"name": "Luther Blissett",
"email": "lb@example.com"
}
Next, write a test for serializing a Person
instance to JSON:
import static io.dropwizard.testing.FixtureHelpers.*;
import static org.assertj.core.api.Assertions.assertThat;
import io.dropwizard.jackson.Jackson;
import org.junit.Test;
import com.fasterxml.jackson.databind.ObjectMapper;
public class PersonTest {
private static final ObjectMapper MAPPER = Jackson.newObjectMapper();
@Test
public void serializesToJSON() throws Exception {
final Person person = new Person("Luther Blissett", "lb@example.com");
final String expected = MAPPER.writeValueAsString(
MAPPER.readValue(fixture("fixtures/person.json"), Person.class));
assertThat(MAPPER.writeValueAsString(person)).isEqualTo(expected);
}
}
This test uses AssertJ assertions and JUnit to test that when a Person
instance is serialized
via Jackson it matches the JSON in the fixture file. (The comparison is done on a normalized JSON
string representation, so formatting doesn’t affect the results.)
Next, write a test for deserializing a Person
instance from JSON:
import static io.dropwizard.testing.FixtureHelpers.*;
import static org.assertj.core.api.Assertions.assertThat;
import io.dropwizard.jackson.Jackson;
import org.junit.Test;
import com.fasterxml.jackson.databind.ObjectMapper;
public class PersonTest {
private static final ObjectMapper MAPPER = Jackson.newObjectMapper();
@Test
public void deserializesFromJSON() throws Exception {
final Person person = new Person("Luther Blissett", "lb@example.com");
assertThat(MAPPER.readValue(fixture("fixtures/person.json"), Person.class))
.isEqualTo(person);
}
}
This test uses AssertJ assertions and JUnit to test that when a Person
instance is
deserialized via Jackson from the specified JSON fixture it matches the given object.
While many resource classes can be tested just by calling the methods on the class in a test, some
resources lend themselves to a more full-stack approach. For these, use ResourceTestRule
, which
loads a given resource instance in an in-memory Jersey server:
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
public class PersonResourceTest {
private static final PeopleStore dao = mock(PeopleStore.class);
@ClassRule
public static final ResourceTestRule resources = ResourceTestRule.builder()
.addResource(new PersonResource(dao))
.build();
private final Person person = new Person("blah", "blah@example.com");
@Before
public void setup() {
when(dao.fetchPerson(eq("blah"))).thenReturn(person);
}
@After
public void tearDown(){
// we have to reset the mock after each test because of the
// @ClassRule, or use a @Rule as mentioned below.
reset(dao);
}
@Test
public void testGetPerson() {
assertThat(resources.target("/person/blah").request().get(Person.class))
.isEqualTo(person);
verify(dao).fetchPerson("blah");
}
}
Instantiate a ResourceTestRule
using its Builder
and add the various resource instances you
want to test via ResourceTestRule.Builder#addResource(Object)
. Use a @ClassRule
annotation
to have the rule wrap the entire test class or the @Rule
annotation to have the rule wrap
each test individually (make sure to remove static final modifier from resources
).
In your tests, use #target(String path)
, which initializes a request to talk to and test
your instances.
This doesn’t require opening a port, but ResourceTestRule
tests will perform all the serialization,
deserialization, and validation that happens inside of the HTTP process.
This also doesn’t require a full integration test. In the above
example, a mocked PeopleStore
is passed to the
PersonResource
instance to isolate it from the database. Not only does this make the test much
faster, but it allows your resource unit tests to test error conditions and edge cases much more
easily.
Hint
You can trust PeopleStore
works because you’ve got working unit tests for it, right?
By default, a ResourceTestRule
will register all the default exception mappers (this behavior is new in 1.0). If
registerDefaultExceptionMappers
in the configuration yaml is planned to be set to false
,
ResourceTestRule.Builder#setRegisterDefaultExceptionMappers(boolean)
will also need to be set to false
. Then,
all custom exception mappers will need to be registered on the builder, similarly to how they are registered in an
Application
class.
Note that the in-memory Jersey test container does not support all features, such as the @Context
injection.
A different test container can be used via
ResourceTestRule.Builder#setTestContainerFactory(TestContainerFactory)
.
For example, if you want to use the Grizzly HTTP server (which supports @Context
injections) you need to add the
dependency for the Jersey Test Framework providers to your Maven POM and set GrizzlyWebTestContainerFactory
as
TestContainerFactory
in your test classes.
<dependency>
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
<version>${jersey.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</exclusion>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
public class ResourceTestWithGrizzly {
@ClassRule
public static final ResourceTestRule RULE = ResourceTestRule.builder()
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new ExampleResource())
.build();
@Test
public void testResource() {
assertThat(RULE.target("/example").request()
.get(String.class))
.isEqualTo("example");
}
}
To avoid circular dependencies in your projects or to speed up test runs, you can test your HTTP client code
by writing a JAX-RS resource as test double and let the DropwizardClientRule
start and stop a simple Dropwizard
application containing your test doubles.
public class CustomClientTest {
@Path("/ping")
public static class PingResource {
@GET
public String ping() {
return "pong";
}
}
@ClassRule
public static final DropwizardClientRule dropwizard = new DropwizardClientRule(new PingResource());
@Test
public void shouldPing() throws IOException {
final URL url = new URL(dropwizard.baseUri() + "/ping");
final String response = new BufferedReader(new InputStreamReader(url.openStream())).readLine();
assertEquals("pong", response);
}
}
Or, for JUnit 5:
@ExtendWith(DropwizardExtensionsSupport.class)
class CustomClientTest {
@Path("/ping")
public static final class PingResource {
@GET
public String ping() {
return "pong";
}
}
private static final DropwizardClientExtension dropwizard = new DropwizardClientExtension(new PingResource());
@Test
void shouldPing() throws IOException {
final URL url = new URL(dropwizard.baseUri() + "/ping");
final String response = new BufferedReader(new InputStreamReader(url.openStream())).readLine();
assertEquals("pong", response);
}
}
Hint
Of course you would use your HTTP client in the @Test
method and not java.net.URL#openStream()
.
The DropwizardClientRule
takes care of:
It can be useful to start up your entire application and hit it with real HTTP requests during testing.
The dropwizard-testing
module offers helper classes for your easily doing so.
The optional dropwizard-client
module offers more helpers, e.g. a custom JerseyClientBuilder,
which is aware of your application’s environment.
Adding DropwizardAppRule
to your JUnit test class will start the app prior to any tests
running and stop it again when they’ve completed (roughly equivalent to having used @BeforeClass
and @AfterClass
).
DropwizardAppRule
also exposes the app’s Configuration
,
Environment
and the app object itself so that these can be queried by the tests.
If you don’t want to use the dropwizard-client
module or find it excessive for testing, you can get access to
a Jersey HTTP client by calling the client method on the rule. The returned client is managed by the rule
and can be reused across tests.
public class LoginAcceptanceTest {
@ClassRule
public static final DropwizardAppRule<TestConfiguration> RULE =
new DropwizardAppRule<>(MyApp.class, ResourceHelpers.resourceFilePath("my-app-config.yaml"));
@Test
public void loginHandlerRedirectsAfterPost() {
Client client = RULE.client();
Response response = client.target(
String.format("http://localhost:%d/login", RULE.getLocalPort()))
.request()
.post(Entity.json(loginForm()));
assertThat(response.getStatus()).isEqualTo(302);
}
}
By creating a DropwizardTestSupport instance in your test you can manually start and stop the app in your tests, you do this by calling its before
and after
methods. DropwizardTestSupport
also exposes the app’s Configuration
, Environment
and the app object itself so that these can be queried by the tests.
public class LoginAcceptanceTest {
public static final DropwizardTestSupport<TestConfiguration> SUPPORT =
new DropwizardTestSupport<TestConfiguration>(MyApp.class,
ResourceHelpers.resourceFilePath("my-app-config.yaml"),
ConfigOverride.config("server.applicationConnectors[0].port", "0") // Optional, if not using a separate testing-specific configuration file, use a randomly selected port
);
@BeforeClass
public void beforeClass() {
SUPPORT.before();
}
@AfterClass
public void afterClass() {
SUPPORT.after();
}
@Test
public void loginHandlerRedirectsAfterPost() {
Client client = new JerseyClientBuilder(SUPPORT.getEnvironment()).build("test client");
Response response = client.target(
String.format("http://localhost:%d/login", SUPPORT.getLocalPort()))
.request()
.post(Entity.json(loginForm()));
assertThat(response.getStatus()).isEqualTo(302);
}
}
Commands can and should be tested, as it’s important to ensure arguments are interpreted correctly, and the output is as expected.
Below is a test for a command that adds the arguments as numbers and outputs the summation to the console. The test ensures that the result printed to the screen is correct by capturing standard out before the command is ran.
public class CommandTest {
private final PrintStream originalOut = System.out;
private final PrintStream originalErr = System.err;
private final InputStream originalIn = System.in;
private final ByteArrayOutputStream stdOut = new ByteArrayOutputStream();
private final ByteArrayOutputStream stdErr = new ByteArrayOutputStream();
private Cli cli;
@Before
public void setUp() throws Exception {
// Setup necessary mock
final JarLocation location = mock(JarLocation.class);
when(location.getVersion()).thenReturn(Optional.of("1.0.0"));
// Add commands you want to test
final Bootstrap<MyConfiguration> bootstrap = new Bootstrap<>(new MyApplication());
bootstrap.addCommand(new MyAddCommand());
// Redirect stdout and stderr to our byte streams
System.setOut(new PrintStream(stdOut));
System.setErr(new PrintStream(stdErr));
// Build what'll run the command and interpret arguments
cli = new Cli(location, bootstrap, stdOut, stdErr);
}
@After
public void teardown() {
System.setOut(originalOut);
System.setErr(originalErr);
System.setIn(originalIn);
}
@Test
public void myAddCanAddThreeNumbersCorrectly() {
final boolean success = cli.run("add", "2", "3", "6");
SoftAssertions softly = new SoftAssertions();
softly.assertThat(success).as("Exit success").isTrue();
// Assert that 2 + 3 + 6 outputs 11
softly.assertThat(stdOut.toString()).as("stdout").isEqualTo("11");
softly.assertThat(stdErr.toString()).as("stderr").isEmpty();
softly.assertAll();
}
}
In Dropwizard, the database access is managed via the @UnitOfWork
annotation used on resource
methods. In case you want to test database-layer code independently, a DAOTestRule
is provided
which setups a Hibernate SessionFactory
.
public class DatabaseTest {
@Rule
public DAOTestRule database = DAOTestRule.newBuilder().addEntityClass(FooEntity.class).build();
private FooDAO fooDAO;
@Before
public void setUp() {
fooDAO = new FooDAO(database.getSessionFactory());
}
@Test
public createsFoo() {
FooEntity fooEntity = new FooEntity("bar");
long id = database.inTransaction(() -> {
return fooDAO.save(fooEntity);
});
assertThat(fooEntity.getId, notNullValue());
}
@Test
public roundtripsFoo() {
long id = database.inTransaction(() -> {
return fooDAO.save(new FooEntity("baz"));
});
FooEntity fooEntity = fooDAO.get(id);
assertThat(fooEntity.getFoo(), equalTo("baz"));
}
}
The DAOTestRule
SessionFactory
instance which can be passed to, e.g., a subclass of AbstractDAO
Configuration objects can be tested for correct deserialization and validation. Using the classes
created in polymorphic configurations as an example, one can
assert the expected widget is deserialized based on the type
field.
public class WidgetFactoryTest {
private final ObjectMapper objectMapper = Jackson.newObjectMapper();
private final Validator validator = Validators.newValidator();
private final YamlConfigurationFactory<WidgetFactory> factory =
new YamlConfigurationFactory<>(WidgetFactory.class, validator, objectMapper, "dw");
@Test
public void isDiscoverable() throws Exception {
// Make sure the types we specified in META-INF gets picked up
assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes())
.contains(HammerFactory.class)
.contains(ChiselFactory.class);
}
@Test
public void testBuildAHammer() throws Exception {
final File yml = new File(Resources.getResource("yaml/hammer.yml").toURI());
final WidgetFactory wid = factory.build(yml);
assertThat(wid).isInstanceOf(HammerFactory.class);
assertThat(((HammerFactory) wid).createWidget().getWeight()).isEqualTo(10);
}
// test for the chisel factory
}