JUnit has been around forever and it is almost the defacto testing framework for Java Developers. But the last version of JUnit 4 was released almost 10 years ago which is a quite a long time and a lot has changed since then.

With the introduction of Java 8 Lambdas, Java 9 Modules, etc., JUnit required a major revamp of the framework. For this reason, JUnit team has released its major version 5 nicknamed Jupiter in 2018. Major goals of JUnit 5 Jupiter has been to achieve the following:

  • Modular
  • Extensible
  • Modern
  • Backward compatible

JUnit 5 is composed of following modules:

  • JUnit Platform (Launcher and TestEngine API) – This is the core for launching testing frameworks on the JVM.
  • JUnit Jupiter (API and Engine) – Newly written Programming and Extension model for JUnit 5.
  • JUnit Vintage (API and Engine) – Testing API and Engine for running JUnit 3, JUnit 4 test cases.
  • Third Party (API and Engine) – Specsy, Spek, Drools Scenario are few to be named. Anyone can write their own.

Let’s look at a small example of a FormatService:

public class FormatService {

    public String append(String main, String suffix) {
        if (main == null || suffix == null) {
            throw new IllegalArgumentException("Both Arguments are required");
        }
        return main + suffix;
    }

    public String prepend(String main, String prefix) {
        if (main == null || prefix == null) {
            throw new IllegalArgumentException("Both Arguments are required");
        }
        return prefix + main;
    }

    public String repeat(String main, int numberOfTimes) {
        if (main == null || numberOfTimes == 0) {
            throw new IllegalArgumentException("Both Arguments are required");
        }
        List items = Collections.nCopies(numberOfTimes, main);
        return String.join("", items);
    }

}

and how it is tested using JUnit 4:

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.Timeout;

import java.util.concurrent.TimeUnit;

import static org.junit.Assert.assertEquals;

public class JUnitFormatServiceTest {

    private FormatService formatService = new FormatService();

    @Rule
    public ExpectedException expectedException = ExpectedException.none();

    @Rule
    public Timeout timeout = new Timeout(15, TimeUnit.MILLISECONDS);

    @Test
    public void testAppend() {
        assertEquals("abcdef", formatService.append("abc", "def"));
    }

    @Test
    public void testPrepend() {
        assertEquals("uvwxyz", formatService.prepend("xyz", "uvw"));
    }

    @Test
    public void testRepeat() {
        assertEquals("blahblahblah", formatService.repeat("blah", 3));
    }

    @Test
    public void testRepeat_timeout() {
        formatService.repeat("blah", 100_000);
    }

    @Test
    public void testAppend_IllegalArgumentException() {
        expectedException.expect(IllegalArgumentException.class);
        formatService.append(null, null);
    }
}

 

This style of testing may be really familiar to many as it uses org.junit.Test annotation and org.junit.Rule annotation along with static methods in org.junit.Assert class to carry out testing scenarios. Some restrictions of Junit 4 are that it required to have a public class for test case as well as public methods for tests. This is not mandatory in JUnit 5 as we will see later.

Now let’s see how to migrate this to a Test case which can be run with JUnit 5 with minimum changes:

import org.junit.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.migrationsupport.rules.EnableRuleMigrationSupport;

import java.time.Duration;

import static org.junit.jupiter.api.Assertions.*;

@EnableRuleMigrationSupport
public class JUnitJupiterMigrationFormatServiceTest {

    private FormatService formatService = new FormatService();

    @Test
    public void testAppend() {
        assertEquals("abcdef", formatService.append("abc", "def"));
    }

    @Test
    public void testPrepend() {
        assertEquals("uvwxyz", formatService.prepend("xyz", "uvw"));
    }

    @Test
    public void testRepeat() {
        assertEquals("blahblahblah", formatService.repeat("blah", 3));
    }

    @Test
    @DisplayName("The Test Didn't finish within the specified time")
    public void testRepeat_timeout() {
        assertTimeout(Duration.ofMillis(10), () -> formatService.repeat("blah", 100_000));
    }

    @Test
    public void testAppend_IllegalArgumentException() {
        IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () -> formatService.append(null, null));
        assertNotNull(iae);
        assertEquals("Both Arguments are required", iae.getMessage());
    }
}

 

As you can see here the code is still almost the same except for the org.junit.jupiter.migrationsupport.rules.EnableRuleMigrationSupport annotation and using of static methods from class org.junit.jupiter.api.Assertions. Now, this Test does exactly same thing using JUnit 5. Some other important things to note are the usage of assertTimeout and assertThrows methods instead of @Rule annotations.

Following shows an 100% JUnit 5 test case with some of its most notable features:

import org.junit.jupiter.api.*;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.util.Collections;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;

class JUnitJupiterFormatServiceTests {

    private FormatService formatService = new FormatService();

    @Test
    @DisabledOnOs(OS.WINDOWS)
    @DisplayName("Not supposed to run on Windows OS")
    void testAppend() {
        assertEquals("abcdef", formatService.append("abc", "def"));
    }

    @Nested
    @DisplayName("Happy Cases")
    public class HappyCases {
        @Test
        @Tag("happy")
        void testAppend() {
            assertEquals("abcdef", formatService.append("abc", "def"));
        }

        @Test
        @Tag("happy")
        void testPrepend() {
            assertEquals("uvwxyz", formatService.prepend("xyz", "uvw"));
        }

        @Test
        @Tag("happy")
        void testRepeat() {
            assertEquals("blahblahblah", formatService.repeat("blah", 3));
        }

    }

    @RepeatedTest(value = 5, name = "පරීක්ෂණ {totalRepetitions} න් {currentRepetition} වන පරීක්ෂණය")
    void repeatingTest(RepetitionInfo repetitionInfo) {
        assertEquals(String.join("", Collections.nCopies(repetitionInfo.getCurrentRepetition(), "clap")), formatService.repeat("clap", repetitionInfo.getCurrentRepetition()));
    }

    @ParameterizedTest
    @ValueSource(strings = {
        " World!",
        " John",
        " Tom"
    })
    void parameterizedTest(String input) {
        assertEquals("Hello"+input, formatService.append("Hello", input));
    }

    @TestFactory
    Stream dynamicTests() {
        final String FILE_NAME = "abc";
        return Stream.of(".jpg", ".png", "tif").map(s -> dynamicTest("Test for "+s, () -> assertEquals(FILE_NAME+s, formatService.append(FILE_NAME, s))));
    }

}


JUnit 5 features in detail:

  • Not required to use public classes and public methods for testing
  • Conditional Enabling or Disabling of Tests – As with testAppend method in above code the annotation @DisabledOnOs(OS.WINDOWS) is used to disable that test from executing on Windows Operating System. There are annotations to conditionally enable tests also.
  • Nested Tests for better grouping of Test cases – As with class HappyCases which groups all the happy path test scenarios.
  • Tagging of Tests – As with tests inside HappyCases. Tagging can be done non-hierarchically across multiple classes as well.
  • Naming of Tests – Using @DisplayName annotation custom names can be given to tests which may contain space, special characters, and even emojis. No need to rely on method name.
  • Test templates – As with repeatingTest method @RepeatedTest can be used to repeatedly test with multiple iterations. RepetitionInfo can be used to obtain more information about the iteration.
  • Parameterized Tests – As with parameterizedTest method annotated with @ParameterizedTest, @ValueSource or @MethodSource can be used to provide input data for test cases.
  • Dynamic Tests – As with dynamicTests method which returns a Stream of DynamicTest objects, Tests can be created without having a custom method at all.

The source code can be found at Github

Shazin Sadakath
Author : Shazin Sadakath
Published Date June 15, 2018