JUnit Rule + Java 8 Repeatable Annotations == Clean Tests


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.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: