Skip to content

Instantly share code, notes, and snippets.

@kimble
Created May 25, 2012 19:15
Show Gist options
  • Save kimble/2789987 to your computer and use it in GitHub Desktop.
Save kimble/2789987 to your computer and use it in GitHub Desktop.
JUnit @rule for running Dropwizard integration test. Ps! This is just a proof of concept. There are probably some landminds lying around waiting to go off, especially around lifecycle management and static state
package com.developerb.dropwizard;
import com.yammer.dropwizard.AbstractService;
import com.yammer.dropwizard.Service;
import com.yammer.dropwizard.cli.Command;
import com.yammer.dropwizard.config.Configuration;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import java.lang.reflect.Method;
import java.net.URI;
/**
* JUnit @Rule that'll start and stop a Dropwizard service around each test method.
*
* This class might be extended with factory methods for pre-configured http client
* instance for both the main and the internal service endpoint.
*
* @author Kim A. Betti <[email protected]>
*/
public class DropwizardTestServer<C extends Configuration, S extends Service<C>> implements TestRule {
private final Class<C> configurationClass;
private final Class<S> serviceClass;
private final String config;
private TestableServerCommand<C> command;
private S service;
protected DropwizardTestServer(Class<C> configClass, Class<S> serviceClass, String config) {
this.configurationClass = configClass;
this.serviceClass = serviceClass;
this.config = config;
}
public static <C extends Configuration, S extends Service<C>> DropwizardTestServer<C, S> testServer(
Class<C> configClass, Class<S> serviceClass, String config) {
return new DropwizardTestServer<>(configClass, serviceClass, config);
}
@Override
public Statement apply(Statement base, Description description) {
return new DropwizardStatement(base);
}
public boolean isRunning() {
return command.isRunning();
}
public S getService() {
return service;
}
public URI getPublicRootUri() {
return command.getRootUriForConnector("main");
}
public URI getInternalRootUri() {
return command.getRootUriForConnector("internal");
}
private class DropwizardStatement extends Statement {
private final Statement base;
public DropwizardStatement(Statement base) {
this.base = base;
}
@Override
public void evaluate() throws Throwable {
service = serviceClass.newInstance();
registerTestCommand(service);
try {
service.run(new String[] { "test-server", config });
base.evaluate();
}
finally {
command.stop();
}
}
/**
* Register a custom command that'll allow us to register our test-server
* startup logic that in turn will let us shut it down in a controlled fashion.
*
* I really don't like using reflection like this, but it's better then introducing
* a new abstract class in the Service class hierarchy solely for testing purposes.
*/
private void registerTestCommand(Service<C> service) throws Exception {
command = new TestableServerCommand<>(configurationClass);
Method method = AbstractService.class.getDeclaredMethod("addCommand", Command.class);
method.setAccessible(true);
method.invoke(service, command);
}
}
}
package com.developerb.dropwizard;
import com.developerb.dropwizard.DropwizardTestServer;
import com.google.inject.Injector;
import Sample.server.SampleConfiguration;
import Sample.server.SampleService;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicResponseHandler;
import org.junit.Rule;
import org.junit.Test;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import static com.developerb.dropwizard.DropwizardTestServer.testServer;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;
/**
* @author Kim A. Betti <[email protected]>
*/
public class SampleIntegrationTest {
@Rule
public DropwizardTestServer<SampleConfiguration, SampleService> testServer
= testServer(SampleConfiguration.class, SampleService.class, "config.yml");
@Test
public void shouldBeRunning() {
assertTrue("Server should be running", testServer.isRunning());
}
@Test
public void weHaveAccessToPublicRootUri() throws URISyntaxException {
URI expectedUri = new URI("http://localhost:8080");
assertEquals(expectedUri, testServer.getPublicRootUri());
}
@Test
public void weAlsoHaveAccessToInternalRootUri() throws URISyntaxException {
URI expectedUri = new URI("http://localhost:8081");
assertEquals(expectedUri, testServer.getInternalRootUri());
}
@Test
public void playingPingPong() throws IOException {
HttpClient httpClient = getHttpClientFromService();
URI root = testServer.getInternalRootUri();
String pingResponseBody = executeSimpleHttpGet(httpClient, root, "/ping");
assertEquals("pong", pingResponseBody.trim());
httpClient.getConnectionManager().shutdown();
}
private String executeSimpleHttpGet(HttpClient httpClient, URI root, String path) throws IOException {
HttpGet initialFetch = new HttpGet(root.toString() + path);
ResponseHandler<String> responseHandler = new BasicResponseHandler();
return httpClient.execute(initialFetch, responseHandler);
}
private HttpClient getHttpClientFromService() {
SampleService service = testServer.getService();
Injector injector = service.getInjector();
return injector.getInstance(HttpClient.class);
}
}
package com.developerb.dropwizard;
import com.yammer.dropwizard.AbstractService;
import com.yammer.dropwizard.cli.ConfiguredCommand;
import com.yammer.dropwizard.config.Configuration;
import com.yammer.dropwizard.config.Environment;
import com.yammer.dropwizard.config.HttpConfiguration;
import com.yammer.dropwizard.config.ServerFactory;
import com.yammer.dropwizard.logging.Log;
import org.apache.commons.cli.CommandLine;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import javax.management.*;
import java.lang.management.ManagementFactory;
import java.net.URI;
import java.net.URISyntaxException;
import static com.google.common.base.Preconditions.checkArgument;
/**
* Normally ServerCommand is in charge of starting the service, but that's not particularly
* well suited for integration testing as it joins the current thread and keeps the Server
* instance to itself.
*
* This implementation is based on the original ServerCommand, but in addition to being
* stoppable it provides a few convenience methods for tests.
*
* @author Kim A. Betti <[email protected]>
*/
public class TestableServerCommand<T extends Configuration> extends ConfiguredCommand<T> {
private final Log log = Log.forClass(TestableServerCommand.class);
private final Class<T> configurationClass;
private Server server;
public TestableServerCommand(Class<T> configurationClass) {
super("test-server", "Starts an HTTP test-server running the service");
this.configurationClass = configurationClass;
}
@Override
protected Class<T> getConfigurationClass() {
return configurationClass;
}
@Override
protected void run(AbstractService<T> service, T configuration, CommandLine params) throws Exception {
server = initializeServer(service, configuration);
try {
server.start();
}
catch (Exception e) {
log.error(e, "Unable to start test-server, shutting down");
server.stop();
}
}
public void stop() throws Exception {
try {
stopJetty();
}
finally {
unRegisterLoggingMBean();
}
}
/**
* We won't be able to run more then a single test in the same JVM instance unless
* we do some tidying and un-register a logging m-bean added by Dropwizard.
*/
private void unRegisterLoggingMBean() throws Exception {
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
ObjectName loggerObjectName = new ObjectName("com.yammer:type=Logging");
if (server.isRegistered(loggerObjectName)) {
server.unregisterMBean(loggerObjectName);
}
}
private void stopJetty() throws Exception {
if (server != null) {
server.stop();
checkArgument(server.isStopped());
}
}
public boolean isRunning() {
return server.isRunning();
}
public URI getRootUriForConnector(String connectorName) {
try {
Connector connector = getConnectorNamed(connectorName);
String host = connector.getHost() != null ? connector.getHost() : "localhost";
return new URI("http://" + host + ":" + connector.getPort());
}
catch (URISyntaxException e) {
throw new IllegalStateException(e);
}
}
private Connector getConnectorNamed(String name) {
Connector[] connectors = server.getConnectors();
for (Connector connector : connectors) {
if (connector.getName().equals(name)) {
return connector;
}
}
throw new IllegalStateException("No connector named " + name);
}
private Server initializeServer(AbstractService<T> service, T configuration) throws Exception {
Environment environment = getInitializedEnvironment(service, configuration);
ServerFactory serverFactory = getServerFactory(service, configuration);
return serverFactory.buildServer(environment);
}
private ServerFactory getServerFactory(AbstractService<T> service, T configuration) {
HttpConfiguration httpConfig = configuration.getHttpConfiguration();
return new ServerFactory(httpConfig, service.getName());
}
private Environment getInitializedEnvironment(AbstractService<T> service, T configuration) throws Exception {
Environment environment = new Environment(configuration, service);
service.initializeWithBundles(configuration, environment);
return environment;
}
}
@spinscale
Copy link

Have you found any other solution except patching dropwizard? Or is there any other trick to circumvent?

@spinscale
Copy link

Dropwizard 0.6.0-SNAPSHOT is fixed regarding the async logger issue. However the rule needs major changes.. I will update the rule when dropwizard 0.6.0 is released, as I got it working after some more hacks

@chadsmall
Copy link

thanks kimble for the work on this. It's proven very nice for a way to integration test DAO's. I see in your SampleIntegrationTest.playingPingPong() test is looks like you implemented Guice w/in DropWizard...

private HttpClient getHttpClientFromService() {
    SampleService service = testServer.getService();
    Injector injector = service.getInjector(); <<<<<<<<<<<<<<<<<<<<<<<
    return injector.getInstance(HttpClient.class);  <<<<<<<<<<<<<<<<<<
}

Did you follow this group thread on your implementation of guice into dropwizard? - https://groups.google.com/forum/#!topic/dropwizard-user/LHCSUT960AE

I also noticed your gist around a Timer annotation using guice, nice - https://gist.github.com/2623833

thanks again for sharing your junit @rule work.

@kimble
Copy link
Author

kimble commented Apr 26, 2013

Oh, I didn't realize that people had been commenting on this! Thanks for all the feedback!

@petetronic
Copy link

Thanks for sharing this. If folks can settle for a class-level JUnit approach for integration testing, Dropwizard 0.6.2 seems to have quietly added a DropwizardServiceRule (see https://github.com/codahale/dropwizard/pull/307/files) for this purpose.

import com.yammer.dropwizard.testing.junit.DropwizardServiceRule;

...

@ClassRule
public static final DropwizardServiceRule<TestConfiguration> RULE = 
    new DropwizardServiceRule<TestConfiguration>(MyService.class, 
        Resources.getResource("my-service-config.yml").getPath());

...

@Test
    public void fooTest() {
        Client client = new Client();
        String root = String.format("http://localhost:%d/", RULE.getLocalPort());
        URI uri = UriBuilder.fromUri(root).path("/foo").build();
        WebResource rs = client.resource(uri);

...

API (without docs, unfortunately) appears to be here too:
http://dropwizard.codahale.com/maven/dropwizard-testing/apidocs/com/yammer/dropwizard/testing/junit/DropwizardServiceRule.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment