JUnit 5 is a huge improvement over previous versions. Check out this post where we explore this version and its new testing features.
JUnit
is the most popular testing framework in Java, and with JUnit
5 testing in Java 8 and beyond, it takes another step forward.
This version was released in September 2017 and has been actively updated to
fix bugs and add new features. Moreover, JUnit 5 is also compatible with
version 3 and 4 by adding junit-vintage-engine to
your classpath path.
Migrating From JUnit 4
When migrating from JUnit 4,
there are a few considerations to bear in mind:
- To have JUnit 5 annotations, you need to add the junit-jupiter-api to your classpath.
- Replace @BeforeClass, @Before , @AfterClass, and @After annotations with @BeforeAll, @BeforeEach, @AfterAll, and @AfterEach alternatively.
@BeforeAll
static void initAll(){
LOGGER.info("@BeforeAll runs once before all tests");
}
@BeforeEach
void init(){
LOGGER.info("@BeforeEach runs before each test");
}
@AfterAll
static void tearDownAll(){
LOGGER.info("@AfterAll runs once after all tests");
}
@AfterEach
void tearDown(){
LOGGER.info("@AfterEach runs after each test");
}
- Replace @Ignore with @Disabled. JUnit 5 also provides a more powerful filter in order to ignore or disable tests by JavaScript expressions, OS, Java version, or system properties
@Test
@Disabled("Disabled test
(it used to be @Ignore in jUnit4)")
void skippedTest(){
//
not executed
}
@Test
@EnabledIf(value = "true", reason = "test runs
because it is true")
void isExecuted(){
}
@DisabledIf("Math.random() )
@RepeatedTest(10)
void sometimesIsExecuted(){
System.out.println("Running "
+ counter++);
}
- @Category needs to be replaced with @Tag.
@Tag("myTestSuite")
class TagsTests{
@Test
@Tag("myTest")
void tagExample(){
}
}
- @RunWith does not exist anymore, use @ExtendWithinstead.
- @Rule and @ClassRule were also removed, use @ExtendWithinstead.
New JUnit 5 Features
@DisplayName
@DisplayName allows you to override a test class or method with a
custom message, special characters, or even emojis.
@DisplayName("Display name
test suite")
class DisplayNamesTests{
@Test
@DisplayName("My Custom Test
Name")
void displayNameTest(){
}
@Test
@DisplayName("\uD83D\uDE03")// Happy Emoji
void displayEmojiTest(){
}
}
Assertions
Assertions
include five new improvements:
- Compatibility with a lambda expression.
assertEquals(Stream.of(1,4,5).mapToInt(Integer::intValue).sum(), 10, "Sum should be 10");
- Group assertions
- assertAll()
ListString>
names = Arrays.asList("Sergio","Juan","Peter");
assertAll("names",
()-> assertEquals("Sergio",
names.get(0)),
()-> assertEquals("Juan", names.get(1)),
()-> assertEquals("Adolfo",
names.get(2)));
- More control over exceptions — now, you can even inspect the returning exception to verify the message, cause, stacktrace using:
- assertThrows()
Throwable
runtimeException = assertThrows(RuntimeException.class,()->{
throw newRuntimeException("exception");
});
assertEquals("exception",
runtimeException.getMessage());
- Timeout assertions:
- assertTimeout()
@Test
void timeoutNotExceeded(){
assertTimeout(Duration.ofMinutes(2),()-> System.out.println("Hello"));
}
@Test
void timeoutNotExceededWithResult(){
String result = assertTimeout(Duration.ofMinutes(2),()->"hello");
assertEquals("hello",
result);
}
@Test
@Disabled("Assertion fails
with exceeded timeout of 2ms by 8ms")
void timeoutExceeded(){
assertTimeout(Duration.ofMillis(2),()->{
sleep(10);
});
}
Assumptions
- assumeTrue()
- assumeFalse()
- assumingThat()
Assumptions
are preconditions that need to be satisfied to run subsequent assertions. If
the assumption fails, TestAbortedException is
thrown and the complete test is skipped.
@Test
void trueAssumption(){
assumeTrue(25);
assertEquals(7,3 + 4);
}
@Test
void falseAssumption(){
assumeFalse(32);
assertEquals(7,4 + 3);
}
@Test
void assumptionThat(){
String word ="word";
assumingThat("word".equals(word),
()-> assertEquals(3, 2 + 1));
}
@Nested
@Nested gives you more freedom to create groups of a related
test in the same test suite.
class NestedTests{
private ListString> strings;
@Nested
class listIsInstantiated{
@BeforeEach
void init(){
strings = new ArrayList();
}
@Test
void listIsEmpty(){
assertTrue(strings.isEmpty());
}
@Nested
class afterAddingString{
@BeforeEach
void init(){
strings.add("hello");
}
@Test
void listIsNotEmpty(){
assertFalse(strings.isEmpty());
}
}
}
}
@BeforeAll and @AfterAll are
only allowed if the test class is annotated with @TestInstance(Lifecycle.PER_CLASS).
Dependency Injection
JUnit 5
allows adding parameters in constructors and test methods. Therefore, now
constructors and methods annotated with @Test, @TestFactory,
@BeforeEach,
@AfterEach, @BeforeAll, @AfterAll accept
parameters. There are three kinds of allowed parameters:
- TestInfo: allows you to get information related to a test suite or test method, such as display name, tags, test class:
DependencyInjectionTests(TestInfo
testInfo){
assertEquals("Dependency Injection Test Suite", testInfo.getDisplayName());
}
@Nested
class testInfoExamples{
@BeforeEach
void init(TestInfo testInfo){
String displayName = testInfo.getDisplayName();
assertTrue(displayName.equals("Test Example")
|| displayName.equals("myTest()"));
}
@Test
@DisplayName("Test
Example")
@Tag("my-tag")
void test(TestInfo testInfo){
assertTrue(testInfo.getTags().contains("my-tag"));
}
@Test
void myTest(){
}
}
- TestReporter: is used to print test information to stout or stderr.
@Test
void testReporterString(TestReporter testReporter){
//
Use test reporter to print information to stout or stderr
testReporter.publishEntry("my message");
}
@Test
void testReporterKeyValue(TestReporter testReporter){
testReporter.publishEntry("myKey","myValue");
}
@Test
void testReporterMap(TestReporter testReporter){
MapString, String> myMap = new HashMap();
myMap.put("myKey","myValue");
testReporter.publishEntry(myMap);
}
- RepetitionInfo: can be used only on tests annotated with @RepeatedTest and provides information regarding the current repetition number or total of repetitions.
private static int repetition = 1;
@RepeatedTest(10)
@DisplayName("repetition")
void repetitionTest(RepetitionInfo repetitionInfo){
assertEquals(repetition++, repetitionInfo.getCurrentRepetition());
assertEquals(10, repetitionInfo.getTotalRepetitions());
}
Interfaces and Default Methods
Interfaces
create contracts to implement in your test suites. Methods declared as default
in an interface will always run in a test suite.
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
interface Interfaces{
@BeforeAll
default void initAll(){
System.out.println("@BeforeAll runs once before all tests");
}
@BeforeEach
default void init(){
System.out.println("@BeforeEach runs before each test");
}
@AfterAll
default void tearDownAll(){
System.out.println("@AfterAll runs once after all tests");
}
@AfterEach
default void tearDown(){
System.out.println("@AfterEach runs after each test");
}
}
class ClassImplementingInterfaceTests implements Interfaces {
@Test
void myTest(){
}
}
@TestInstance(TestInstance.Lifecycle.PER_CLASS) is
required to make @BeforeAll and @AfterAll work.
@RepeatedTest
@RepeatedTest allows you to repeat a test as many
times as you want by passing the number of desired repetitions to the
annotation.
@RepeatedTest(value = 10, name = "{displayName}
{currentRepetition}/{totalRepetitions}")
void repetitionTest(){
}
@ParameterizedTest
When
using the @ParameterizedTest annotation
in combination with...
- @ValueSource
- @EnumSource
- @MethodSource
- @CsvSource
- @CsvFileSource
- @ArgumentsSource
You can
run a test multiple times with different inputs each time, as shown below:
classParameterizedTests{
enum MyEnum {RED, GREEN, BLACK}
@ParameterizedTest
@ValueSource(ints ={2,4,8})
void integerParameters(int value){
assertEquals(0, value % 2);
}
@ParameterizedTest
@EnumSource(value = MyEnum.class, names = {"RED","GREEN"})
void integerParameters(MyEnum myEnum){
assertTrue(EnumSet.of(MyEnum.RED, MyEnum.GREEN).contains(myEnum));
}
@ParameterizedTest
@MethodSource("myStrings")
voidmethodSourceProvider(String string){
assertTrue(Arrays.asList("hello","world").contains(string));
}
static ListString>myStrings(){
return Arrays.asList("hello","world");
}
}
For
@MethodSource, if
the method has the same name as a test, the parameter name is not required.
IMPORTANT:
to make use of this feature, junit-jupiter-params needs
to be added to the classpath.
Conclusion
As you
can see, JUnit 5 is a huge improvement over previous versions
and introduces many new testing features. It also allows you to write more
expressive units test when using in combination with lambda expressions from
Java 8.
For
more examples, check out this repository in GitHub.