Exploring Spring Boot Testing
Example 1 TestCases: Calculator add(int x, int y): int
Example 2 TestCases: StringUtils captilize(String data): String
1. Add Maven dependencies for Junit
// pom.xml
<dependencies>
// ......
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
</dependencies>
2. Create Applicaion Code
// src/main/java/com/example/junitlearndemo/DemoUtils.java
public class DemoUtils {
public int add(int x, int y) {
return x + y;
}
public Object checkNull(Object object) {
return object;
}
}
3. Create Unit Testing Code
Unit tests have the following structure:
// src/test/java/com/example/junitlearndemo/DemoUtilsTest.java
public class DemoUtilsTest {
@Test
void testDemoUtilsAdd(){
// set up
DemoUtils utils = new DemoUtils();
int expected = 6;
int unexpected = 8;
// execute
int actual = utils.add(2, 4);
// assert
Assertions.assertEquals(expected, actual, "2+4 must equal 6");
Assertions.assertNotEquals(unexpected, actual, "2+4 must eaual 8");
}
@Test
void testCheckNull(){
// set up
DemoUtils utils = new DemoUtils();
String s1 = "hello";
String s2 = null;
Assertions.assertNotNull(s1, "Object should not be null");
Assertions.assertNull(s2, "Object should be null");
}
}
When developing tests, we may need to perform common operations.
When developing test, we may need to perform one-time operations.
We’d like to give custom diaplay names, because of 3 reasons:
class xxxTest{
@Test
@DisplayName("test add function")
void testDemoUtilsAdd() {
// ....
}
}
But I wish Junit could generate a display name for me, How could we do? Display Name Generators
// demo code
@DisplayNameGeneration(DisplayNameGenerator.Simple.class)
class xxxTest{
// ...
}
// com/example/junitlearndemo/DemoUtils.java
public class DemoUtils {
// ...
public String throwException(int a) throws Exception {
if (a < 0) {
throw new Exception("Value should be greater or equal to 0");
}
return "Value is greater or equal to 0";
}
public void checkTimeout() throws InterruptedException {
System.out.println("it's going to sleep!");
Thread.sleep(2000);
System.out.println("sleep over!");
}
}
Unit Testing code:
// com/example/junitlearndemo/DemoUtilsTest.java
public class DemoUtilsTest {
// ......
@Test
@DisplayName("Throw and not throws")
void testThrowException(){
DemoUtils utils = new DemoUtils();
Assertions.assertThrows(Exception.class, ()-> {utils.throwException(-1);}, "should throw exception");
Assertions.assertDoesNotThrow(() -> {utils.throwException(9);}, "should not throw exception");
}
@Test
@DisplayName("test timeout")
void testCheckTimeout(){
DemoUtils utils = new DemoUtils();
Assertions.assertTimeout(Duration.ofSeconds(3), utils::checkTimeout, "Method should execute in 3 seconds");
}
}
In general, order should not be a factor in unit tests, there should no dependency between tests, all tests should pass regardless of the order in which they are run.However, there are some use cases when we want to control the order.
How to configure the order/sort algorithm for the test methods?
Annotation @TestMethodOrder
How many kinds of order algorithms?
MethodOrderer.DisplayName
: sorts test methods alphaumerically based on display name.MethodOrderer.MethodName
: sorts test methods alphaumerically based on method name.MethodOrderer.Random
: pseudo-random order based on method name.MethodOrderer.OrderAnnotation
: sort test methods numerically based on @Order
annotation.Example for MethodOrderer.OrderAnnotation
the display order is: testCheckNull(-2) > testDemoUtilsAdd(-1) > testThrowException(1) > testCheckTimeout(5)
// com/example/junitlearndemo/DemoUtilsTest.java
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class DemoUtilsTest {
@Test
@Order(-1)
void testDemoUtilsAdd() {
// ...
}
@Test
@Order(-2)
void testCheckNull() {
// ...
}
@Test
@DisplayName("Throw and not throws")
@Order(1)
void testThrowException(){
// ...
}
@Test
@DisplayName("test timeout")
@Order(5)
void testCheckTimeout(){
// ...
}
}
Code Coverage measures how many methods/lines are called by tests. On most teams, 70%~80% is acceptable.
Run Unit Test with Code Coverage
Generate Test Report with IntelliJ
Useful when running as part of DevOps build process.
Install Maven in MacOS
Download Maven or using brew install mvn
.
(base) fan@fandeMacBook-Pro junit-learn-demo % mvn --version
Apache Maven 3.8.4
Run unit tests
(base) fan@fandeMacBook-Pro junit-learn-demo % mvn clean test
# ...
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.019 s - in com.example.junitlearndemo.DemoUtilsTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 5.104 s
Generate unit test reports
configure the pom.xml
// ....
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-surefire-report-pluginartifactId>
<version>3.0.0version>
<executions>
<execution>
<phase>testphase>
<goals>
<goal>reportgoal>
goals>
execution>
executions>
plugin>
plugins>
// ...
rerun the mvn code
# run tests and execute Surfire report plugin to generate html report.
mvn clean test
# site: add website resources(images, css, etc).
# -DgenerateReports=false don't overwrite existing html reports.
mvn site -DgenerateReports=false
view the html report in target/site/surefire-report.html
.
Handling test failure
By default, Maven Surefire plugin will not generate reports if tests fail. So we need to add configuration in pom.xml
.
// ...
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-surefire-pluginartifactId>
<version>3.0.0version>
<configuration>
<testFailureIgnore>truetestFailureIgnore>
configuration>
plugin>
// ...
Rerun the code and view the report, we can build success and show failure tests in report.
mvn clean test
mvn site -DgenerateReports=false
Show @DisplayName in reports
By default, Maven Surefire plugin will not show @DisplayName in reports. So we need to add configuration in pom.xml
.
// ...
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-surefire-pluginartifactId>
<version>3.0.0version>
<configuration>
<testFailureIgnore>truetestFailureIgnore>
<statelessTestsetReporter implementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5Xml30StatelessReporter">
<usePhrasedTestCaseMethodName>trueusePhrasedTestCaseMethodName>
statelessTestsetReporter>
configuration>
plugin>
// ...
Generate code coverage reports
JaCoCo(Java Code Coverage) is a free code coverage library for Java. So we need to add configuration in pom.xml
, run the code mvn clean test
, view the html in target/site/jacoco/index.html
.
<plugin>
<groupId>org.jacocogroupId>
<artifactId>jacoco-maven-pluginartifactId>
<version>0.8.7version>
<executions>
<execution>
<id>jacoco-prepareid>
<goals>
<goal>prepare-agentgoal>
goals>
execution>
<execution>
<id>jacoco-reportid>
<phase>testphase>
<goals>
<goal>reportgoal>
goals>
execution>
executions>
plugin>
We may not need to run all of the tests, there are some use cases:
@Disabled
: disable a test method.@EnabledOnOs
: enable test when running on a given OS.// com/example/junitlearndemo/ConditionalTest.java
public class ConditionalTest {
@Test
@Disabled("don't run until jira #111 is resolved")
public void basicTest() {
System.out.println("basic test");
}
@Test
@EnabledOnOs(OS.WINDOWS)
public void testForWindowsOnly() {
System.out.println("testForWindowsOnly");
}
@Test
@EnabledOnOs(OS.MAC)
public void testForMACOnly() {
System.out.println("testForMACOnly");
}
@Test
@EnabledOnOs({OS.MAC, OS.WINDOWS})
public void testForWindowsMAC() {
System.out.println("testForWindowsMAC");
}
}
@EnabledOnJre
: enable test for a given java version.@EnabledForJreRange
: enable test for a given java version range.// com/example/junitlearndemo/ConditionalTest.java
public class ConditionalTest {
// ...
@Test
@EnabledOnJre(JRE.JAVA_17)
public void testForJava17Only() {
System.out.println("testForJava17Only");
}
@Test
@EnabledForJreRange(min = JRE.JAVA_8, max = JRE.JAVA_18)
public void testForJava8To18() {
System.out.println("testForJava8To18");
}
@Test
@EnabledForJreRange(min = JRE.JAVA_18)
public void testForMinJava18() {
System.out.println("testForMinJava18");
}
}
@EnabledIfSystemProperty
: enable test based on System Property.@EnabledIfEnvironmentVariable
: enable test based on Environment Variable.How to set System Property and Environment Variable in IntelliJ?
public class ConditionalTest {
// ...
@Test
@EnabledIfSystemProperty(named = "SYS_PROP", matches = "CI_CD_DEPLOY")
public void testOnlyForSystemProperty(){
System.out.println("testOnlyForSystemProperty");
}
@Test
@EnabledIfEnvironmentVariable(named = "ENV", matches = "DEV")
public void testOnlyForEnvironmentVariable(){
System.out.println("testOnlyForEnvironmentVariable");
}
}