package io.dropwizard.testing.junit;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.dropwizard.Application;
import io.dropwizard.Configuration;
import io.dropwizard.cli.Command;
import io.dropwizard.cli.ServerCommand;
import io.dropwizard.lifecycle.Managed;
import io.dropwizard.setup.Environment;
import io.dropwizard.testing.ConfigOverride;
import io.dropwizard.testing.DropwizardTestSupport;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.JerseyClientBuilder;
import org.junit.rules.ExternalResource;
import javax.annotation.Nullable;
import javax.ws.rs.client.Client;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
/**
* A JUnit rule for starting and stopping your application at the start and end of a test class.
* <p>
* By default, the {@link Application} will be constructed using reflection to invoke the nullary
* constructor. If your application does not provide a public nullary constructor, you will need to
* override the {@link #newApplication()} method to provide your application instance(s).
* </p>
*
* <p>
* Using DropwizardAppRule at the suite level can speed up test runs, as the application is only started and stopped
* once for the entire suite:
* </p>
*
* <pre>
* @RunWith(Suite.class)
* @SuiteClasses({FooTest.class, BarTest.class})
* public class MySuite {
* @ClassRule
* public static final DropwizardAppRule<MyConfig> DROPWIZARD = new DropwizardAppRule<>(...);
* }
* </pre>
*
* <p>
* If the same instance of DropwizardAppRule is reused at the suite- and class-level, then the application will be
* started and stopped once, regardless of whether the entire suite or a single test is executed.
* </p>
*
* <pre>
* public class FooTest {
* @ClassRule public static final DropwizardAppRule<MyConfig> DROPWIZARD = MySuite.DROPWIZARD;
*
* public void testFoo() { ... }
* }
*
* public class BarTest {
* @ClassRule public static final DropwizardAppRule<MyConfig> DROPWIZARD = MySuite.DROPWIZARD;
*
* public void testBar() { ... }
* }
* </pre>
*
* <p>
*
* </p>
*
* @param <C> the configuration type
*/
public class DropwizardAppRule<C extends Configuration> extends ExternalResource {
private static final int DEFAULT_CONNECT_TIMEOUT_MS = 1000;
private static final int DEFAULT_READ_TIMEOUT_MS = 5000;
private final DropwizardTestSupport<C> testSupport;
private final AtomicInteger recursiveCallCount = new AtomicInteger(0);
private Client client;
public DropwizardAppRule(Class<? extends Application<C>> applicationClass) {
this(applicationClass, (String) null);
}
public DropwizardAppRule(Class<? extends Application<C>> applicationClass,
@Nullable String configPath,
ConfigOverride... configOverrides) {
this(applicationClass, configPath, Optional.empty(), configOverrides);
}
public DropwizardAppRule(Class<? extends Application<C>> applicationClass, String configPath,
Optional<String> customPropertyPrefix, ConfigOverride... configOverrides) {
this(applicationClass, configPath, customPropertyPrefix, ServerCommand::new, configOverrides);
}
public DropwizardAppRule(Class<? extends Application<C>> applicationClass, String configPath,
Optional<String> customPropertyPrefix, Function<Application<C>,
Command> commandInstantiator, ConfigOverride... configOverrides) {
this(new DropwizardTestSupport<>(applicationClass, configPath, customPropertyPrefix, commandInstantiator,
configOverrides));
}
/**
* Alternate constructor that allows specifying exact Configuration object to
* use, instead of reading a resource and binding it as Configuration object.
*
* @since 0.9
*/
public DropwizardAppRule(Class<? extends Application<C>> applicationClass,
C configuration) {
this(new DropwizardTestSupport<>(applicationClass, configuration));
}
/**
* Alternate constructor that allows specifying the command the Dropwizard application is started with.
*
* @since 1.1.0
*/
public DropwizardAppRule(Class<? extends Application<C>> applicationClass,
C configuration, Function<Application<C>, Command> commandInstantiator) {
this(new DropwizardTestSupport<>(applicationClass, configuration, commandInstantiator));
}
public DropwizardAppRule(DropwizardTestSupport<C> testSupport) {
this.testSupport = testSupport;
}
public DropwizardAppRule<C> addListener(final ServiceListener<C> listener) {
this.testSupport.addListener(new DropwizardTestSupport.ServiceListener<C>() {
@Override
public void onRun(C configuration, Environment environment, DropwizardTestSupport<C> rule) throws Exception {
listener.onRun(configuration, environment, DropwizardAppRule.this);
}
@Override
public void onStop(DropwizardTestSupport<C> rule) throws Exception {
listener.onStop(DropwizardAppRule.this);
}
});
return this;
}
public DropwizardAppRule<C> manage(final Managed managed) {
return addListener(new ServiceListener<C>() {
@Override
public void onRun(C configuration, Environment environment, DropwizardAppRule<C> rule) throws Exception {
environment.lifecycle().manage(managed);
}
});
}
@Override
protected void before() {
if (recursiveCallCount.getAndIncrement() == 0) {
testSupport.before();
}
}
@Override
protected void after() {
if (recursiveCallCount.decrementAndGet() == 0) {
testSupport.after();
synchronized (this) {
if (client != null) {
client.close();
}
}
}
}
public C getConfiguration() {
return testSupport.getConfiguration();
}
public int getLocalPort() {
return testSupport.getLocalPort();
}
public int getPort(int connectorIndex) {
return testSupport.getPort(connectorIndex);
}
public int getAdminPort() {
return testSupport.getAdminPort();
}
public Application<C> newApplication() {
return testSupport.newApplication();
}
@SuppressWarnings("unchecked")
public <A extends Application<C>> A getApplication() {
return testSupport.getApplication();
}
public Environment getEnvironment() {
return testSupport.getEnvironment();
}
public ObjectMapper getObjectMapper() {
return testSupport.getObjectMapper();
}
public abstract static class ServiceListener<T extends Configuration> {
public void onRun(T configuration, Environment environment, DropwizardAppRule<T> rule) throws Exception {
// Default NOP
}
public void onStop(DropwizardAppRule<T> rule) throws Exception {
// Default NOP
}
}
public DropwizardTestSupport<C> getTestSupport() {
return testSupport;
}
/**
* Returns a new HTTP Jersey {@link Client} for performing HTTP requests against the tested
* Dropwizard server. The client can be reused across different tests and automatically
* closed along with the server. The client can be augmented by overriding the
* {@link #clientBuilder()} method.
*
* @return a new {@link Client} managed by the rule.
*/
public Client client() {
synchronized (this) {
if (client == null) {
client = clientBuilder().build();
}
return client;
}
}
protected JerseyClientBuilder clientBuilder() {
return new JerseyClientBuilder()
.property(ClientProperties.CONNECT_TIMEOUT, DEFAULT_CONNECT_TIMEOUT_MS)
.property(ClientProperties.READ_TIMEOUT, DEFAULT_READ_TIMEOUT_MS);
}
}