JUnit 5
Table of contents
- Add JUnit 5
- Create the first test
- IntelliJ and
@DisplayName
- Parameterized test
- Load test data from files (
@CsvFileSource
) - Custom converters
- Tests tagging
- Nested tests
- How to verify that an exception is thrown?
- Test lifecycle
- Recommended reading
- Miscellaneous
Add JUnit 5
Add the junit-jupiter aggregator dependency and configure that
test
task to make use of JUnit 5.dependencies { testImplementation 'org.junit.jupiter:junit-jupiter:5.7.0-M1' } test { useJUnitPlatform() }
The Maven repository make use of older Gradle dependency configurations. Use the new dependency configuration
testImplementation
that replaced thetestCompile
dependency configuration, as the latter is now deprecated.Use the latest version when possible.
You may need to refresh Gradle to see the changes in the IDE.
You can press the
[command] + [shift] + [I]
shortcut to refresh Gradle and update the external dependencies. The new libraries will be included under the External Libraries, a shown next.
Create the first test
A player is playing a dice game that requires 10 or more to win. Write a method that determines whether the player has won. This method will take two integers as an input and will return true
if the sum of the given integers is equal to or greater than 10
.
Open the
App.java
class, press[command] + [shift] + [T]
and select the Create New Test… menu optionA warning will appear if the test directory is missing.
Click Cancel and create the
test
directory undersrc
folder first.Create the test class.
A blank test class under the
test
directory will be created.package demo; import static org.junit.jupiter.api.Assertions.*; class AppTest { }
Please refer to Gradle documentation for more information about the project structure.
Provide a better description
Update file:
src/test/java/demo/AppTest.java
ⓘ NoteThe following test class uses the package-private access modifier. JUnit 4 required the class and test method to bepublic
, which is not required in JUnit 5 to improve test encapsulation.package demo; import org.junit.jupiter.api.DisplayName; @DisplayName( "Dice game test" ) class AppTest { }
Write the first test
Update file:
src/test/java/demo/AppTest.java
ⓘ NoteThe test method does not have thestatic
key word.package demo; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.fail; @DisplayName( "Dice game test" ) class AppTest { @Test @DisplayName( "should return true if the sum of the given integers is equal to or greater than 10, false otherwise" ) void shouldReturnTrueIfWon() { fail( "Simulating error" ); } }
This test fails on purpose.
Run the test
$ ./gradlew clean test > Task :test FAILED Dice game test > should return true if the sum of the given integers is equal to or greater than 10, false otherwise FAILED org.opentest4j.AssertionFailedError at AppTest.java:14 ... BUILD FAILED in 1s 4 actionable tasks: 2 executed, 2 up-to-date
Add the proper assertion
Update file:
src/test/java/demo/AppTest.java
⚠ The following example does not compile!!package demo; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertTrue; @DisplayName( "Dice game test" ) class AppTest { @Test @DisplayName( "should return true if the sum of the given integers is equal to or greater than 10, false otherwise" ) void shouldReturnTrueIfWon() { assertTrue( App.hasWon( 5, 5 ) ); } }
Add a basic implementation (that throws an exception when invoked on purpose)
Update file:
src/main/java/demo/App.java
package demo; public class App { public static boolean hasWon( final int a, final int b ) { throw new UnsupportedOperationException("Coming soon..."); } }
Run the tests
$ ./gradlew clean test
The test will fail as expected.
Implement the functionality
Update file:
src/main/java/demo/App.java
package demo; public class App { public static boolean hasWon( final int a, final int b ) { return a + b >= 10; } }
ⓘ NoteNote that in TDD, we would simply returntrue
(just enough to make the test passes).Run the tests again, this time these should pass.
$ ./gradlew clean test BUILD SUCCESSFUL in 1s 4 actionable tasks: 4 executed
The output does not include the tests that were executed.
Update file:
build.gradle
test { useJUnitPlatform() testLogging { events = ['FAILED', 'PASSED', 'SKIPPED', 'STANDARD_OUT'] } }
Run the tests again.
ⓘ NoteIt is important to run theclean
Gradle task as otherwise the tests may not run given that no changes are made to the code.$ ./gradlew clean test > Task :test Dice game test > should return true if the sum of the given integers is equal to or greater than 10, false otherwise PASSED
IntelliJ and @DisplayName
IntelliJ may not pick up the @DisplayName
annotation values. as shown next.
This can be easily fixed.
Open IntelliJ preferences and filter for
gradle
, as shown nextⓘ NotePress[command] + [,]
to open the preferences for any application running on a Mac OS.Our project is using Gradle to build, run and test our project.
Change the Run tests using to IntelliJ, as shown next.
Run the test from within IntelliJ
Open the test class
AppTest
and press[control] + [shift] + [R]
The tests names should not reflect the
@DisplayName
annotation values.
Parameterized test
The following example makes use of the @CsvSource
annotation as we have multiple parameters. In case of single parameters, the @ValueSource
annotation can be used.
Convert the test to make use of parameters instead
Update file:
src/test/java/demo/AppTest.java
ⓘ NoteThe@Test
annotation is replaced by the@ParameterizedTest
annotation.package demo; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import static org.junit.jupiter.api.Assertions.assertTrue; @DisplayName( "Dice game test" ) class AppTest { @CsvSource( { "5, 5", "5, 6", "6, 5", "6, 6" } ) @ParameterizedTest( name = "Dice values {0} and {1} should yield a victory" ) @DisplayName( "should return true if the sum of the given integers is equal to or greater than 10, false otherwise" ) void shouldReturnTrueIfWon( int a, int b ) { assertTrue( App.hasWon( a, b ) ); } }
Run the test
$ ./gradlew clean test
Note that the test will be executed 4 times, one for each line
> Task :test Dice game test > Dice values 5 and 5 should yield a victory PASSED Dice game test > Dice values 5 and 6 should yield a victory PASSED Dice game test > Dice values 6 and 5 should yield a victory PASSED Dice game test > Dice values 6 and 6 should yield a victory PASSED
Load test data from files (@CsvFileSource
)
Instead of putting all the inputs in the source code, we can put the inputs in a CSV file and have these loaded by the test, using the @CsvFileSource
annotation.
Create the test sample file
Create file:
src/test/resources/samples/game_won.csv
Die 1,Die 2 5,5 5,6 6,5 6,6
IntelliJ treats CSV files differently, as shown in the following image.
A small table like icon will appear at the top right corner and two tabs are shown at the bottom left corner of the editor.
Click on the table like icon to open the CSV preferences for this file.
The data tab is now selected, as shown next.
We can easily edit the CSV using the data tab which will automatically add the delimiters for us.
Use the CSV file
package demo; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvFileSource; import static org.junit.jupiter.api.Assertions.assertTrue; @DisplayName( "Dice game test" ) class AppTest { @CsvFileSource( resources = "/samples/game_won.csv", numLinesToSkip = 1 ) @ParameterizedTest( name = "Dice values {0} and {1} should yield a victory" ) @DisplayName( "should return true if the sum of the given integers is equal to or greater than 10, false otherwise" ) void shouldReturnTrueIfWon( int a, int b ) { assertTrue( App.hasWon( a, b ) ); } }
The
@CsvSource
annotation is replaced by the@CsvFileSource
annotation.@CsvFileSource( resources = "/samples/game_won.csv", numLinesToSkip = 1 )
The first line is skipped as this is the CSV header line and it should not be used as parameter.
Custom converters
In some applications, negative numbers are wrapped in round brackets. For example, negative ten is represented as (10)
.
package demo;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
@DisplayName( "Custom negative number conversion" )
class AppTest {
@CsvSource( { "(10), -10" } )
@ParameterizedTest( name = "The value ''{0}'' should be converted to {1}" )
void shouldConvertInput( int actual, int expected ) {
assertEquals( actual, expected );
}
}
The above will fail to convert the value (10)
to -10
$ ./gradlew clean test
> Task :test FAILED
Custom negative number conversion > The value (10) should be converted to -10 FAILED
org.junit.jupiter.api.extension.ParameterResolutionException at ParameterizedTestMethodContext.java:273
Caused by: org.junit.jupiter.params.converter.ArgumentConversionException at DefaultArgumentConverter.java:122
Caused by: java.lang.NumberFormatException at NumberFormatException.java:68
1 test completed, 1 failed
...
BUILD FAILED in 1s
5 actionable tasks: 5 executed
Create a custom converter
package demo; import org.junit.jupiter.params.converter.ArgumentConversionException; import org.junit.jupiter.params.converter.DefaultArgumentConverter; import org.junit.jupiter.params.converter.SimpleArgumentConverter; import java.util.regex.Pattern; public final class CustomNumberConverter extends SimpleArgumentConverter { private static final Pattern REGEX = Pattern.compile( "\\(\\d+\\)" ); @Override protected Object convert( final Object source, final Class<?> targetType ) throws ArgumentConversionException { final String updated = replaceRoundBracketsWithMinus( source ); return DefaultArgumentConverter.INSTANCE.convert( updated, targetType ); } private String replaceRoundBracketsWithMinus( final Object source ) { final String value = source.toString(); if ( REGEX.matcher( value ).matches() ) { return "-" + value.subSequence( 1, value.length() - 1 ); } return value; } }
Use the custom converter
package demo; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.converter.ConvertWith; import org.junit.jupiter.params.provider.CsvSource; import static org.junit.jupiter.api.Assertions.assertEquals; @DisplayName( "Custom negative number conversion" ) class AppTest { @ParameterizedTest( name = "The value ''{0}'' should be converted to {1}" ) @CsvSource( { "(1), -1", "(10), -10", "(100), -100", "(1000), -1000", "10, 10" } ) void shouldConvertInput( @ConvertWith( CustomNumberConverter.class ) int actual, int expected ) { assertEquals( actual, expected ); } }
The first argument is annotated with the
@ConvertWith
. JUnit will use this converter to convert the string CSV value to integer.Run the test
$ ./gradlew clean test ... Custom negative number conversion > The value '(1)' should be converted to -1 PASSED Custom negative number conversion > The value '(10)' should be converted to -10 PASSED Custom negative number conversion > The value '(100)' should be converted to -100 PASSED Custom negative number conversion > The value '(1000)' should be converted to -1000 PASSED Custom negative number conversion > The value '10' should be converted to 10 PASSED BUILD SUCCESSFUL in 2s 5 actionable tasks: 5 executed
The values are converted using our converter and then by the default converter.
Tests tagging
Consider the following test class.
package demo;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@DisplayName( "Playing with tags" )
class AppTest {
@Test
@DisplayName( "should always run this test" )
void shouldRunTest() {
}
@Test
@DisplayName( "should only run this test when the slow tag is active" )
void shouldRunSlowTest() {
}
}
The test class shown above has two (blank) test methods. The second test method, shouldRunSlowTest()
is a slow running test. We may not want to run this test everytime as it would slow development down. With that said, we would like to control when this test runs.
JUnit 5 supports tags which can be used to group tests and only run the tests that we want.
Run the tests
$ ./gradlew clean test > Task :test Playing with tags > should only run this test when the slow tag is active PASSED Playing with tags > should always run this test PASSED BUILD SUCCESSFUL in 2s 5 actionable tasks: 5 executed
Both tests are executed.
Tag the slow test with the
@Tag
annotationUpdate file:
src/test/java/demo/AppTest.java
package demo; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @DisplayName( "Playing with tags" ) class AppTest { @Test @DisplayName( "should always run this test" ) void shouldRunTest() { } @Test @Tag( "slow" ) @DisplayName( "should only run this test when the slow tag is active" ) void shouldRunSlowTest() { } }
The
@Tag
annotation takes the tag name as its value,"slow"
in our example.Exclude the slow tests
Update file:
build.gradle
test { useJUnitPlatform { excludeTags "slow" } testLogging { events = ['FAILED', 'PASSED', 'SKIPPED', 'STANDARD_OUT'] } }
Re-run the tests. This time, the slow tests should not be included.
$ ./gradlew clean test > Task :test Playing with tags > should always run this test PASSED BUILD SUCCESSFUL in 2s 5 actionable tasks: 5 executed
Define a new test type,
slowTest
Update file:
build.gradle
task slowTest(type: Test) { description = 'Runs the slow tests.' group = 'verification' useJUnitPlatform { includeTags "slow" } testLogging { events = ['FAILED', 'PASSED', 'SKIPPED', 'STANDARD_OUT'] } shouldRunAfter test }
List all gradle tasks.
$ ./gradlew tasks
The
slowTest
should be listed under the Verification tasks, as shown next.Verification tasks ------------------ check - Runs all checks. slowTest - Runs the slow tests. test - Runs the unit tests.
Run the slow tests
./gradlew clean slowTest > Task :slowTest Playing with tags > should only run this test when the slow tag is active PASSED BUILD SUCCESSFUL in 2s 5 actionable tasks: 5 executed
Add the
slowTest
Gradle task to the build flowOur new Gradle task is not part of the build flow. Running
./gradlew build
will not run the slow tests. List the Gradle tasks on whichbuild
depends.$ ./gradlew build taskTree
The
build
task depends on thecheck
task, which depends on thetest
task, as shown next.:build +--- :assemble ... \--- :check \--- :test +--- :classes | +--- :compileJava | \--- :processResources \--- :testClasses +--- :compileTestJava | \--- :classes | +--- :compileJava | \--- :processResources \--- :processTestResources
Make the Gradle
check
task depend on the GradleslowTest
task.check { dependsOn slowTest }
Verify that the Gradle
check
task depends on theslowTest
task.$ ./gradlew check taskTree
The Gradle
check
task depends on both thetest
and theslowTest
tasks.:check +--- :slowTest | +--- :classes | | +--- :compileJava | | \--- :processResources | \--- :testClasses | +--- :compileTestJava | | \--- :classes | | +--- :compileJava | | \--- :processResources | \--- :processTestResources \--- :test +--- :classes | +--- :compileJava | \--- :processResources \--- :testClasses +--- :compileTestJava | \--- :classes | +--- :compileJava | \--- :processResources \--- :processTestResources
Run both tests
./gradlew clean check > Task :test Playing with tags > should always run this test PASSED > Task :slowTest Playing with tags > should only run this test when the slow tag is active PASSED BUILD SUCCESSFUL in 2s 6 actionable tasks: 6 executed
Custom annotations
a
package demo; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Test @Tag( "slow" ) @Retention( RetentionPolicy.RUNTIME ) @Target( { ElementType.METHOD } ) public @interface SlowTest { }
b
package demo; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @DisplayName( "Playing with tags" ) class AppTest { @Test @DisplayName( "should always run this test" ) void shouldRunTest() { } @SlowTest @DisplayName( "should only run this test when the slow tag is active" ) void shouldRunSlowTest() { } }
c
$ ./gradlew clean check > Task :test Playing with tags > should always run this test PASSED > Task :slowTest Playing with tags > should only run this test when the slow tag is active PASSED BUILD SUCCESSFUL in 3s 6 actionable tasks: 6 executed
Nested tests
How to verify that an exception is thrown?
Guava is a set of core Java libraries from Google that includes new collection types (such as multimap and multiset), immutable collections, a graph library, and utilities for concurrency, I/O, hashing, caching, primitives, strings, and more! It is widely used on most Java projects within Google, and widely used by many other companies as well.
dependencies {
implementation 'com.google.guava:guava:29.0-jre'
}
Example
package demo;
import com.google.common.base.Preconditions;
public class App {
public static boolean hasWon( final int a, final int b ) {
Preconditions.checkArgument( a >= 1 && a <= 6, "Invalid dice value %d", a );
Preconditions.checkArgument( b >= 1 && b <= 6, "Invalid dice value %d", b );
return a + b >= 10;
}
}
Passing any invalid value will cause the function to throw an IllegalArgumentException
.
App.hasWon( 7 ,1);
Exception
Exception in thread "main" java.lang.IllegalArgumentException: Invalid dice value 7
at com.google.common.base.Preconditions.checkArgument(Preconditions.java:190)
Testing
package demo;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class AppTest {
@ParameterizedTest( name = "should throw an IllegalArgumentException({0}) when provided the invalid dice values {1} and {2}" )
@CsvSource( {
"Invalid dice value 7, 7, 1",
"Invalid dice value 7, 1, 7",
"Invalid dice value 0, 0, 7",
} )
void shouldThrowAnExceptionWhen( String expectedErrorMessage, int a, int b ) {
final IllegalArgumentException exception = assertThrows( IllegalArgumentException.class, () -> App.hasWon( a, b ) );
assertEquals( expectedErrorMessage, exception.getMessage() );
}
}
The above example runs three tests.
$ ./gradlew clean test
> Task :test
AppTest > should throw an IllegalArgumentException(Invalid dice value 7) when provided the invalid dice value 7 PASSED
AppTest > should throw an IllegalArgumentException(Invalid dice value 7) when provided the invalid dice value 1 PASSED
AppTest > should throw an IllegalArgumentException(Invalid dice value 0) when provided the invalid dice value 0 PASSED
Test lifecycle
JUnit 5 provides a set of powerful annotations that help managing the tests lifecycle.
Example
package demo;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class AppTest {
@Test
public void test1() {
System.out.println( "@Test 1" );
}
@Test
public void test2() {
System.out.println( "@Test 2" );
}
@BeforeAll
public static void setupOnce() {
System.out.println( "@BeforeAll" );
}
@AfterAll
public static void teardownOnce() {
System.out.println( "@AfterAll" );
}
@BeforeEach
public void setupBeforeEachTest() {
System.out.println( "@BeforeEach" );
}
@AfterEach
public void teardownAfterEachTest() {
System.out.println( "@AfterEach" );
}
}
Run the tests
$ ./gradlew clean test
Will show how each method is called.
> Task :test
AppTest STANDARD_OUT
@BeforeAll
AppTest > test1() STANDARD_OUT
@BeforeEach
@Test 1
@AfterEach
AppTest > test1() PASSED
AppTest > test2() STANDARD_OUT
@BeforeEach
@Test 2
@AfterEach
AppTest > test2() PASSED
AppTest STANDARD_OUT
@AfterAll
BUILD SUCCESSFUL in 1s
4 actionable tasks: 4 executed
Note that the methods invoked by an @AfterEach
are called even when the tests fail. Also, note that here there is a mixture of static
and non static
methods.
Recommended reading
- Mastering Software Testing with JUnit 5 (O’Reilly Books)
- Pragmatic Unit Testing in Java 8 with JUnit (O’Reilly Books)
Miscellaneous
@Disabled
TestInfo
TestReporter
MethodOrderer
IgnoreCondition
assertAll()
- Assumptions class in Junit 5 :
- Assumptions.assumeTrue() – If the condition is true, then run the test, else aborting the test.
- Assumptions.false() – If the condition is false, then run the test, else aborting the test.
- Assumptions.assumingThat() – is much more flexible, If condition is true then executes, else do not abort test continue rest of code in test.
- TestNG (alternative to JUnit)
- add an aggregator example