Junit 5 Architecture

  • Junit Engine/Junit Jupiter Engine (core engine of Junit)
    • Junit Jupiter API
    • Junit Vintage (For legacy versions)
    • Junit Extentions

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

Import dependency to pom.xml

<dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>RELEASE</version>
      <scope>test</scope>
    </dependency>
  </dependencies> 

General

  • @Test annotation is to mark a method as test

  • assertEquals(expected, actual) methods to test equality

    @Test
    private void testAdd() {
        int actual = mathUtils.add(0, 1);
        assertEquals(1, actual, "Should add two numbers");
    }
    
  • public static <T extends Throwable> T assertThrows(Class<T> expectedType, Executable executable) method to check for exception thrown by a method

    • Here Executable is a @FunctionalInterface with one method void execute() throws Throwable;
    @Test
    private void testDivide() {
        Executable executable = () -> mathUtils.divide(10, 0);
        assertThrows(ArithmeticException.class, executable, "Divide by 0 should return excpetion");
    }
    
  • fail() method will mark a test as fail.

  • New instance created per test

    • By default Junit creates new instance of the call for every test, before running every method that marked as @Test of that particular class
    • But this can be changed using @TestInstance(TestInstance.Lifecycle.PER_CLASS) which is not suggested ❌
      • @TestInstance(TestInstance.Lifecycle.PER_CLASS) doesn’t guarantee order though ‼️
      • we don’t need to use static for @BeforeAll when using PER_CLASS strategy 🤦🏻‍♂️

Life Cycle Hooks ♻️

  • Junit provides 4 lifecycle hooks
    • @BeforeAll - Runs before the instance is created to must be a static method
    • @BeforeEach - Runs before every @Test method, helpful for initializing method variables
    • @AfterAll - Tear down method, runs after every @Test method
    • @AfterEach - It is used to signal that the annotated method should be executed after each @Test method in the current class.

Useful Annotations (Good to know)

  • @DisplayName - Change the name of the test instead showing method name

  • @Dsiabled - Skip test method

    @Test
    @Disabled
    @DisplayName("Woring on TDD, Shoud skipped for now")
    private void incompleMethod() {
        double actual = mathUtils.calculateCircleArea(10);
        double expected = Math.PI * 100;
        assertEquals(expected, actual);
    }
    

Conditional Execution

  • @EnableOnOs(OS.LINUX)

    @Test
    @EnabledOnOs(OS.LINUX)
    @DisplayName("Should Skip as I'm not using Linux")
    public void enableONLinux() {
        fail("On Linux this test should fail");
    }
    
  • @EnableOrJre(JRE.JAVA_11)

  • @EnableIf

  • @EanbleIfSystemProperty

  • @EnableIfEnvironmentVariable

Handling external factors

  • @assumeTrue - Run when an assumption is true otherwise skip this method

    @Test
    public void runIfServerIsUp() {
        assumeTrue(serverIsUp);
        // write your test here;
    }
    

Using @Nested 🎁

Group multiple tests for a method with @Nested

@Nested
@DisplayName("add method")
class AddTest {
    @Test
    @DisplayName("when adding two positive numbers")
    public void testAddPositive() {
        int actual = mathUtils.add(0, 1);
        assertEquals(1, actual, "should return right sum");
    }

    @Test
    @DisplayName("when adding two negative numbers")
    public void testAddNegative() {
        int actual = mathUtils.add(-10, -10);
        assertEquals(-20, actual, "should return right sum");
    }
}

Using assertAll

Multiple assertions inside a method. Can group multiple test without create separate methods.

@Test
@DisplayName("Test Multiply method using assertAll")
public void testMultiply() {
    assertAll(
            () -> assertEquals(4, mathUtils.multiply(2, 2)),
            () -> assertEquals(0, mathUtils.multiply(2, 0)),
            () -> assertEquals(-9, mathUtils.multiply(-3, 3))
    );
}

Lazy loading message using supplier

a supplier can be passed to assert methods, it will run only when the test fails. Can save time for complex message construction.

@Test
@DisplayName("Calculate Circle Area will fail")
public void circleAreaWithWrongValueOfPI() {
    double actual = mathUtils.calculateCircleArea(10);
    double rightExpected = 3.1417 * 100;
    double wrongExpected = Math.PI * 100;
    assertEquals(rightExpected, actual, () -> "Supplier will construct this message lazily if test fails");
    assertEquals(wrongExpected, actual, () -> "Supplier will construct this message lazily, fails at " + Instant.now().toString());
}

Repeat test with @RepetedTest 🔄

@ReptedTest() can run a single test multiple times, the test will successful if all the repeated test is successful.

Junit will inject a RepetitionInfo object to the method marked as @RepetedTest() where information of repetition will reside.

@RepeatedTest(3)
@DisplayName("Test Multiply method should be tested 3 times")
public void testMultiplyMultipleTimes(RepetitionInfo repetitionInfo) {
    assertEquals(4, mathUtils.multiply(2, 2));
}

Use @Tag for tagging test methods

When there is lots of test then @Tag can be helpful. Tagging different test and run them based on tag. Then Change run configuration to run test for certain tag.

Junit tag configuration

Also maven-surefire-plugin can be configured to group and run test by tags.

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.0.0-M1</version>
    <configuration>
        <groups>math</groups>
    </configuration>
</plugin>

TestInfo and TestReporter

Junit can inject two handy class call TestInfo and TestReporter that can be used to print details about the test.

private TestInfo testInfo;
private TestReporter testReporter;

@BeforeEach
public void init(TestInfo testInfo, TestReporter testReporter) {
    System.out.println("Before Each Method");
    this.testInfo = testInfo;
    this.testReporter = testReporter;
    mathUtils = new MathUtils();
    serverIsUp = false;
}

@Test
@DisplayName("Test Multiply method using assertAll")
@Tag("math")
public void testMultiply() {
    testReporter.publishEntry("Running " + testInfo.getDisplayName() + " with tag " + testInfo.getTags());
    assertAll(
            () -> assertEquals(4, mathUtils.multiply(2, 2)),
            () -> assertEquals(0, mathUtils.multiply(2, 0)),
            () -> assertEquals(-9, mathUtils.multiply(-3, 3))
    );
}

Avoid ☠️

  • Do not order your test methods, It is not logics to order test methods. Every test should be independent.
  • Dont modify member variable inside a test, it will not work as JUnit by default create instance of test class before running every test.

Source Code