Created
May 25, 2012 19:15
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
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
Oh, I didn't realize that people had been commenting on this! Thanks for all the feedback!