-
-
Save kimble/2789987 to your computer and use it in GitHub Desktop.
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; | |
} | |
} |
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?
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.
Have you found any other solution except patching dropwizard? Or is there any other trick to circumvent?
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
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.
Oh, I didn't realize that people had been commenting on this! Thanks for all the feedback!
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
An improved solution would be to hook into the lifecycle management, but the environment instance isn't very easy to get hold of.