When writing integration tests, you might have to run a third party server or middleware. Your tests should remain fast to run and you should be able to run them from your IDE.
Docker seems a good choice for this task!
I just published a small library that contains a JUnit rule allowing you to start Docker containers before your unit tests.
If that sounds of interest to you, you should give it a try and tell me what you think!
JUnit rules
JUnit rules allow us to do some sort of AOP applied to JUnit test. Within a rule you are given the handle of the test to run.
You can decide what to do with it. Should we skip it? Should we run it? Should we wrap it in a try catch? Should we add some behavior before or after the test?
You can use the @Rule
annotation to run the rule before each test or the
@ClassRule
annotation to run it once in your test class.
You can have has many rules as you need in your any of your tests.
It is much more powerful than creating an abstract test class from which test will inherit. This is the application of the composition over inheritance principle.
Here is an example of a JUnit rule:
import com.rabbitmq.client.ConnectionFactory;
import org.junit.ClassRule;
import org.junit.Test;
import rules.RabbitContainerRule;
public class RabbitIntegrationTest {
@ClassRule
public static RabbitContainerRule rabbitContainerRule = new RabbitContainerRule();
@Test
public void testConnectsToDocker() throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(rabbitContainerRule.getDockerHost());
factory.setPort(rabbitContainerRule.getRabbitServicePort());
factory.newConnection();
}
}
Behind the scene
Did you know that the Docker daemon is accessible via a REST API? In fact when you use the docker client, it sends HTTP requests to the daemon.
That also means that we can create other docker clients in any programming language. In Java, Spotify has open-sourced a great docker client.
We will use this library to create our JUnit rule. Here is a simplified version of what we will be doing:
public class DockerContainerRule extends ExternalResource {
public DockerContainerRule(String imageName, String[] ports, String cmd) {
dockerClient = createDockerClient();
ContainerConfig containerConfig = createContainerConfig(imageName, ports, cmd);
dockerClient.pull(imageName);
container = dockerClient.createContainer(containerConfig);
}
@Override
protected void before() throws Throwable {
super.before();
dockerClient.startContainer(container.id());
}
@Override
protected void after() {
super.after();
dockerClient.killContainer(container.id());
dockerClient.removeContainer(container.id(), true);
dockerClient.close();
}
}
Simple, isn’t it? You can check out the full code here
This class will allow users to create their own rules, extending this one:
public class RabbitContainerRule extends DockerContainerRule {
public static final String RABBIT_CONTAINER_IMAGE_NAME = "rabbitmq:management";
public RabbitContainerRule() {
// List the ports to open on the container.
// They will automatically be bound to random unused ports on your host
super(RABBIT_CONTAINER_IMAGE_NAME, new String[]{"5672", "15672"});
}
@Override
protected void before() throws Throwable {
super.before();
// wait for container to boot
waitForPort(getRabbitServicePort());
}
public int getRabbitServicePort() {
return getHostPort("5672/tcp");
}
public int getRabbitManagementPort() {
return getHostPort("15672/tcp");
}
}
Bonus
There is an annoying thing with docker containers: you cannot tell if the process running inside is in a ready state and waiting for your to use it or if it is still booting.
Most people use netcat
on a specific
port to wait for a container.
In Java, we can do the same thing with good old sockets!
public void waitForPort(int port, long timeoutInMillis) {
SocketAddress address = new InetSocketAddress(getDockerHost(), port);
long totalWait = 0;
while (true) {
try {
SocketChannel.open(address);
return;
} catch (IOException e) {
try {
Thread.sleep(100);
totalWait += 100;
if (totalWait > timeoutInMillis) {
throw new IllegalStateException("Timeout while waiting for port " + port);
}
} catch (InterruptedException ie) {
throw new IllegalStateException(ie);
}
}
}
}
Conclusion
JUnit rules are a very cool way to improve the readability and the expressiveness of our tests. Check out the system rules for a good example.
Don’t forget to give a try to the project, which is available on github and give me your feedback.