Last couple of months I am spending most of my free time writing Docker Java REST API client using RxJava and OkHttp. I have been following TDD for developing this API. Some of the test cases in RxDockerClientTest have to first create a docker container and then they perform other operations. For example, shouldStartCreatedContainer
test case will test that API can start a created container. Similarly, there are test cases that need a container. One common solution to achieve this is to use @Before setUp
and @After tearDown
methods that take care of creating a container before test case is executed and removing the container after test execution. The problem with this solution is that container will be created for every test case in the test class. I only wanted to create a container for test cases that need it.
If you are new to Java 8 then you can checkout my Java 8 tutorial.
To solve this I decided to write my own custom JUnit rule. JUnit rule will make use of an annotation if annotation is present on a test method then rule will be applied else it will be skipped.
Writing your custom JUnit Rule
import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; public class DockerContainerRule implements TestRule { @Override public Statement apply(Statement base, Description description) { System.out.println("Applying DockerContainerRule to " + description.getMethodName()); return base; } }
Write a test method
public class MyTestCase { @Rule public DockerContainerRule rule = new DockerContainerRule(); @Test public void shouldDoSth1() throws Exception { assertTrue(true); } @Test public void shouldDoSth2() throws Exception { assertTrue(true); } }
When you will run the test case you will see following in the console output.
Applying DockerContainerRule to shouldDoSth1 Applying DockerContainerRule to shouldDoSth2
What we wanted was that rule should only be applied for shouldDoSth1
test case. To achieve this, we can write an annotation that DockerContainerRule
will use to see if it should be applied or not.
Let’s create an annotation CreateDockerContainer
that we will apply to our test method.
import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface CreateDockerContainer { public String container(); }
Now we can use the annotation on our test case as shown below.
@Test @CreateDockerContainer(container = "my_awesome_container") public void shouldDoSth1() throws Exception { assertTrue(true); }
We will update DockerContainerRule
to work only if test method contains CreateDockerContainer
annotation else just return the original statement.
import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; public class DockerContainerRule implements TestRule { @Override public Statement apply(Statement base, Description description) { CreateDockerContainer containerAnnotation = description.getAnnotation(CreateDockerContainer.class); if (containerAnnotation != null) { System.out.println("Applying DockerContainerRule to " + description.getMethodName()); System.out.println("Creating container >> " + containerAnnotation.container()); } return base; } }
If you now run the test case, you will see output for only shouldDoSth1
test.
Applying DockerContainerRule to shouldDoSth1 Creating container >> my_awesome_container
Creating multiple containers
It might also be a common use-case that you would like to create multiple containers. Java version prior to 8 did not allowed you to use the same annotation multiple times on a location. So, you are force to creating container annotations like the one shown below.
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface CreateDockerContainers { public String[] containers(); }
Then you can use in test case like as shown below.
@Test @CreateDockerContainers(containers = {"my_awesome_container_1", "my_awesome_container_2"}) public void shouldDoSth2() throws Exception { assertTrue(true); }
You can update the DockerContainerRule
as shown below to handle this.
import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; import java.util.Arrays; public class DockerContainerRule implements TestRule { @Override public Statement apply(Statement base, Description description) { CreateDockerContainers containerAnnotation = description.getAnnotation(CreateDockerContainers.class); if (containerAnnotation != null) { System.out.println("Applying DockerContainerRule to " + description.getMethodName()); Arrays.stream(containerAnnotation.containers()) .forEach(c -> System.out.println("Creating container >> " + c)); } return base; } }
Wouldn’t be great if we could use CreateDockerContainer
annotation twice instead of using CreateDockerContainers
annotation. Java 8 enhanced annotation mechanism by adding support for repeatable annotation.
To write your own repeatable annotation you have to do the following:
Step 1: Add @Repeatable
annotation to CreateDockerContainer
annotation as shown below. @Repeatable
annotation requires you to specify a mandatory value for the container type that will contain the annotation. We will create container annotation in step 2.
import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Repeatable(CreateDockerContainers.class) public @interface CreateDockerContainer { public String container(); }
Step 2: Create a container annotation that holds the annotation.
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface CreateDockerContainers { public CreateDockerContainer[] value(); }
Now you can use the annotation multiple times on any method as shown below.
@Test @CreateDockerContainer(container = "my_awesome_container_1") @CreateDockerContainer(container = "my_awesome_container_2") public void shouldDoSth2() throws Exception { assertTrue(true); }
Now, you have to update DockerContainerRule
to work with repeatable annotation. Java 8 added a method getAnnotationsByType
that can be used to find all the repeatable annotation on a method.
import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; import java.util.Arrays; public class DockerContainerRule implements TestRule { @Override public Statement apply(Statement base, Description description) { try { CreateDockerContainer[] containerAnnotations = description.getTestClass().getDeclaredMethod(description.getMethodName()).getAnnotationsByType(CreateDockerContainer.class); if (containerAnnotations != null && containerAnnotations.length > 0) { System.out.println("Applying DockerContainerRule to " + description.getMethodName()); Arrays.stream(containerAnnotations).map(ca -> ca.container()) .forEach(containerName -> System.out.println("Creating container >> " + containerName)); } } catch (NoSuchMethodException e) { throw new RuntimeException(e); } return base; } }
Now if we run the test case we will see desired behaviour.
Applying DockerContainerRule to shouldDoSth1 Creating container >> my_awesome_container Applying DockerContainerRule to shouldDoSth2 Creating container >> my_awesome_container_1 Creating container >> my_awesome_container_2
if you want to see how I am creating real docker container then you can refer to rx-docker-client code.