JUnit Test Groups for More Reliable Development
Join the DZone community and get the full member experience.
Join For FreeIntroduction
As a product is being developed and maintained its test suite is enriched as features and functionalities are added and enhanced. Ideally, development teams should aim having a lot of quick unit tests that are run whenever modifications in the code are made. It is great if these are written before or at least together with the tested code, cover as many use cases as possible and finish running after a reasonable amount of time. By all means, a reasonable amount of time is an entity difficult to quantify.
On the other hand, there are a lot of live products that solve quite complex business problems whose thorough verification concerns cross-cutting through multiple application layers at once and thus, the number of integration tests is significant. Running all these unit and integration tests whenever the code is touched is not always feasible, as the productivity and the development speed is decreased considerably. Again, productivity and development speed are hard to quantify but should be always traded for correctness.
Under such circumstances, developers need to imagine possible compromises that help delivering confidently and reliably. One such compromise is the ability to split quick and small unit tests from the heavier integration tests.
This post is going to present a small working example that describes how to split tests into multiple categories (groups) and accomplish the above mentioned compromise.
Context
‘testcategories’ is a small project especially developed for the purpose of this post, using the following:
- Java 11
- Apache Maven 3.6.3
- JUnit 4.13
- Maven Surefire Plugin 2.22.0
It contains two classes each simulating the retrieval of a certain category of entities available from two different sources. They are great sports people, either Romanian or international.
xxxxxxxxxx
public class Local {
public List<String> champions() {
System.out.println("Retrieving local champions...");
return List.of("Simona Halep", "Cristina Neagu");
}
}
public class Remote {
public List<String> champions() {
try {
System.out.println("Retrieving remote champions...");
Thread.sleep(5000);
return List.of("Roger Federer", "Dan Carter");
} catch (InterruptedException e) {
throw new RuntimeException("Champions unavailable.");
}
}
}
For the sake of this example, it is imagined that the ‘local’ ones are easy to reach, cheaper to retrieve in terms of the involved resources, while the ‘remote’ ones more expensive. This is simulated by forcing the designated method to last at least 5 seconds, by putting the current thread to sleep.
Under these circumstances, it is reasonable to state that Local#champions()
method could be unit tested, while the Remote#champions()
one integration tested.
Solutions
In order to split unit and integration tests into distinct categories, there are at least two approaches. Both may be accomplished with the help of ‘maven-surefire-plugin’ by configuring it accordingly.
- Adopt a naming convention for the test classes, depending on the test category they belong to and configure Maven Surefire Plugin so that it includes / excludes what is executed in each scenario.
xxxxxxxxxx
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.0</version>
<configuration>
<includes>
<include>**/*Test.java</include>
</includes>
<excludes>
<exclude>**/*Integration.java</exclude>
</excludes>
</configuration>
</plugin>
It is expected that classes whose names end in ‘Test’ will designate unit tests, while those suffixed with ‘Integration’, integration tests.
I have experimented this solution before, it’s been working great for years, the only drawback I find is that one cannot have both unit and integration tests residing in the same class.
- Annotate classes or tests inside a class with JUnit’s @Category and categorize all or a single test as being part of a certain group designated by a custom marker interface. Moreover, configure Maven Surefire Plugin to run the desired group(s) of tests.
xxxxxxxxxx
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.0</version>
<configuration>
<groups>com.hcd.testcategories.config.UnitTests</groups>
</configuration>
</plugin>
As part of this post, the latter solution is detailed.
Split Unit and Integration Test
The sample project contains 5 tests that are organized in 4 different classe so that multiple scenarios can be presented. In order to split them, two categories are defined as two marker interfaces, whose names were chosen so that it is clear what they designate.
xxxxxxxxxx
public interface UnitTests {
}
public interface IntegrationTests {
}
In order for a class to be considered part of a group, it is enough to annotate it with @Category
and provide as value the corresponding marker interface – @Category(UnitTests.class)
.
Moreover, one can put a test into multiple groups by providing the value as comma separated marker interfaces – @Category({SlowTests.class, DatabaseTests.class})
.
Nevertheless, in the example illustrated here, it is a nonsense to consider a test both as unit and integration in the same time.
The mentioned tests are detailed below.
- LocalTest – contains 1 unit test – class is annotated with
@Category(UnitTests.class)
xxxxxxxxxx
UnitTests.class) (
public class LocalTest {
public void champions() {
final Local local = new Local();
List<String> result = local.champions();
assertEquals(2, result.size());
assertTrue(result.contains("Simona Halep"));
}
}
- RemoteTest – contains 1 integration test – class is annotated with
@Category(IntegrationTests.class)
xxxxxxxxxx
IntegrationTests.class) (
public class RemoteTest {
public void champions() {
final Remote remote = new Remote();
List<String> result = remote.champions();
assertEquals(2, result.size());
assertTrue(result.contains("Roger Federer"));
}
}
- MixedTest – contains 1 unit and 1 integration test – class is not annotated, but each test is annotated with
@Category(UnitTests.class)
and@Category(IntegrationTests.class)
respectively
xxxxxxxxxx
public class MixedTest {
UnitTests.class) (
public void localChampions() {
final Local local = new Local();
List<String> result = local.champions();
assertEquals(2, result.size());
assertTrue(result.contains("Simona Halep"));
}
IntegrationTests.class) (
public void remoteChampions() {
final Remote remote = new Remote();
List<String> result = remote.champions();
assertEquals(2, result.size());
assertTrue(result.contains("Roger Federer"));
}
}
- NewLocalTest – contains 1 unit test – neither the class, nor the test is annotated
xxxxxxxxxx
public class NewLocalTest {
public void champions() {
final Local local = new Local();
List<String> result = local.champions();
assertEquals(2, result.size());
assertTrue(result.contains("Cristina Neagu"));
}
}
The intention with the last one is to designate a test for which the developer has forgotten to annotate it and thus, accidentally left it out of any of the two categories (groups). Such a situation is very likely to appear in real life, after the point the existing tests have been split.
Maven Configuration
In order to be easier to run the tests in various scenarios, one can define a Maven property that designates the test categories (groups) taken into account at a certain execution. Moreover, a couple of profiles can be used, so that it is easier to invoke the build / test command.
Below is the complete POM file of the sample project.
xxxxxxxxxx
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.hcd</groupId>
<artifactId>testcategories</artifactId>
<version>0.0.1-SNAPSHOT</version>
<description>Sample Maven Project that differentiate between Unit and Integration tests at runtime.</description>
<properties>
<test.categories/>
</properties>
<profiles>
<profile>
<id>all-tests</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<build.profile.id>all-tests</build.profile.id>
<test.categories/>
</properties>
</profile>
<profile>
<id>unit-tests</id>
<properties>
<build.profile.id>unit-tests</build.profile.id>
<test.categories>com.hcd.testcategories.config.UnitTests</test.categories>
</properties>
</profile>
<profile>
<id>integration-tests</id>
<properties>
<build.profile.id>integration-tests</build.profile.id>
<test.categories>com.hcd.testcategories.config.IntegrationTests</test.categories>
</properties>
</profile>
</profiles>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<release>11</release>
<includeEmptyDirs>true</includeEmptyDirs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.0</version>
<configuration>
<groups>${test.categories}</groups>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
The profiles defined are:
- ‘all-tests’ – default profile, executes all unit, integration and non-categorized tests. test.categories property is empty.
- ‘unit-tests’ – executes all annotated unit tests. test.categories = com.hcd.testcategories.config.UnitTests.
- ‘integration-tests’ – executes all annotated integration tests. test.categories = com.hcd.testcategories.config.IntegrationTests.
Running Scenarios
Run all defined tests – unit, integration and non-categorized
- by invoking the default profile implicitly
> mvn clean
test
- by invoking the ‘all-tests’ profile
> mvn clean
test
-Pall-tests
- by specifying an empty ‘test.categories’ property
> mvn clean
test
-Dtest.categories=
All 5 tests are executed. The output is below.
xxxxxxxxxx
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.hcd.testcategories.LocalTest
Retrieving local champions...
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.107 s - in com.hcd.testcategories.LocalTest
[INFO] Running com.hcd.testcategories.MixedTest
Retrieving remote champions...
Retrieving local champions...
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.959 s - in com.hcd.testcategories.MixedTest
[INFO] Running com.hcd.testcategories.NewLocalTest
Retrieving local champions...
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.007 s - in com.hcd.testcategories.NewLocalTest
[INFO] Running com.hcd.testcategories.RemoteTest
Retrieving remote champions...
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.964 s - in com.hcd.testcategories.RemoteTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 14.651 s
[INFO] Finished at: 2020-11-12T12:21:31+02:00
[INFO] ------------------------------------------------------------------------
Run all annotated tests – unit and integration
- by specifying the groups (comma separated) as values for the ‘test.categories’ property
> mvn clean
test
-Dtest.categories=com.hcd.testcategories.config.UnitTests,com.hcd.testcategories.config.IntegrationTests
All 4 annotated tests are executed. The non-annotated one in NewLocalTest
is not. The output is below.
xxxxxxxxxx
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.hcd.testcategories.LocalTest
Retrieving local champions...
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.014 s - in com.hcd.testcategories.LocalTest
[INFO] Running com.hcd.testcategories.MixedTest
Retrieving remote champions...
Retrieving local champions...
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.942 s - in com.hcd.testcategories.MixedTest
[INFO] Running com.hcd.testcategories.RemoteTest
Retrieving remote champions...
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.986 s - in com.hcd.testcategories.RemoteTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 14.580 s
[INFO] Finished at: 2020-11-12T13:08:21+02:00
[INFO] ------------------------------------------------------------------------
Run all annotated unit tests
- by invoking the ‘unit-tests’ profile
> mvn clean
test
-Punit-tests
- by specifying the unit tests group as a value for the ‘test.categories’ property
> mvn clean
test
-Dtest.categories=com.hcd.testcategories.config.UnitTests
Both tests that are in the custom UnitTests
category are executed – one from the LocalTest
annotated class and one from the non-anotated MixedTest
class, but annotated at method level. The output is below.
xxxxxxxxxx
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.hcd.testcategories.LocalTest
Retrieving local champions...
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.02 s - in com.hcd.testcategories.LocalTest
[INFO] Running com.hcd.testcategories.MixedTest
Retrieving local champions...
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.008 s - in com.hcd.testcategories.MixedTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 5.891 s
[INFO] Finished at: 2020-11-12T13:27:19+02:00
[INFO] ------------------------------------------------------------------------
Run all annotated integration tests
- by invoking the ‘integration-tests’ profile
> mvn clean
test
-Pintegration-tests
- by specifying the integration tests group as a value for the ‘test.categories’ property
> mvn clean
test
-Dtest.categories=com.hcd.testcategories.config.IntegrationTests
Both tests that are in the custom IntegrationTests
category are executed – one from the RemoteTest
annotated class and one from the non-anotated MixedTest
class, but annotated at method level. The output is below.
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.hcd.testcategories.MixedTest
Retrieving remote champions...
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.993 s - in com.hcd.testcategories.MixedTest
[INFO] Running com.hcd.testcategories.RemoteTest
Retrieving remote champions...
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.967 s - in com.hcd.testcategories.RemoteTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 15.140 s
[INFO] Finished at: 2020-11-12T13:46:22+02:00
[INFO] ------------------------------------------------------------------------
Conclusions
- The quickest execution is, without any doubt, the one that runs just the explicitly marked unit tests.
- The most thorough execution is the one that runs all defined tests (irrespective of the defined groups / categories); it definitely takes longer, but it is the most reliable one in terms of confidence.
- No written test will cease to run during the default execution if for example a developer forgets to annotate either the containing test class or the test method (see point 1. and
NewLocalTest
class in the previous section). - One should apply each of the presented scenarios, depending on the particular circumstance and the purpose of the aimed achievement. During development the former seems to be the proper candidate, while the latter is more suitable for pre-production builds.
Code
The sample project is available now in GitHub - testcategories
Published at DZone with permission of Horatiu Dan. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments