Skip to content

Instantly share code, notes, and snippets.

@kimble
Created May 25, 2012 19:15
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • 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 <kim@developer-b.com>
*/
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 <kim@developer-b.com>
*/
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 <kim@developer-b.com>
*/
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;
}
}
@kimble
Copy link
Author

kimble commented May 26, 2012

An improved solution would be to hook into the lifecycle management, but the environment instance isn't very easy to get hold of.

@rnalam
Copy link

rnalam commented Oct 5, 2012

First, thanks for the gist! Was trying this out, and for the first time, it works fine.
Next time it runs, it just hangs. Figured out it was hanging during the logging configuration.

Here's the call trace when it hangs - and for convenience, i've copy pasted the line corresponding to the stacktrace entry next to it.

com.yammer.dropwizard.config.LoggingFactory.configureLevels() line: 101 (root.getLoggerContext().reset())
com.yammer.dropwizard.config.LoggingFactory.configure() line: 50    (final Logger root = configureLevels();)    
TestableServerCommand<T>(ConfiguredCommand<T>).run(AbstractService<?>, CommandLine) line: 97    (com.yammer.dropwizard.cli.Command: new LoggingFactory(configuration.getLoggingConfiguration(), service.getName()).configure();)
TestableServerCommand<T>(Command).run(AbstractService<?>, String[]) line: 111   (com.yammer.dropwizard.cli.Command: run(checkNotNull(service), cmdLine);)

Would you have any idea what's it that might be wrong here?

@rnalam
Copy link

rnalam commented Oct 5, 2012

Got it, was an issue with AsyncAppender of dropwizard.logging.
It was not interrupting the logging thread before joining in the stop() method, hence was waiting on it. Patched the AsyncAppender with an interrupt statement before joining, and it's working fine now.

@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