SpringBoot -- JUnit 5 for unit testing

Article directory:

1. Changes in JUnit 5

2. JUnit 5 common notes and tests

2.1 @DisplayName,@Disabled,@BeforeEach,@AfterEach,@BeforeAll,@AfterAll

2.2 @Timeout

2.3 @RepeatedTest

3. Assertion

3.1 simple assertion

3.2 array assertion

3.3 combined assertions

3.4 exception assertion

3.5 timeout assertion

3.6 rapid failure

4. Preconditions

5. Nested test

6. Parametric test

7.JUnit4 → Junit5

1. Changes in JUnit 5

Spring boot version 2.2.0 began to introduce JUnit 5 as the default library for unit testing

As the latest version of JUnit framework, JUnit 5 is very different from the previous version of JUnit framework. It consists of several different modules of three different subprojects.

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

JUnit Platform: Junit Platform is the basis for starting the test framework on the JVM. It supports not only Junit's self-made test engine, but also other test engines.

JUnit Jupiter: JUnit Jupiter provides a new programming model of JUnit 5 and is the core of JUnit 5's new features. A test engine is included internally to run on the Junit Platform.

JUnit Vintage: since JUint has developed for many years, in order to take care of old projects, JUnit Vintage provides a test engine compatible with JUnit 4. X and JUnit 3. X.

Note: springboot versions above 2.4 remove the default dependency on Vintage. If you need to be compatible with junit4, you need to introduce it yourself (you can't use the function @ Test of junit4)

JUnit 5's vintage engine removed from spring boot starter test. If you need to continue to be compatible with junit4, you need to introduce vintage by yourself

<dependency>
    <groupId>org.junit.vintage</groupId>
    <artifactId>junit-vintage-engine</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

If you only use JUnit 5 features, you need to add the following dependencies.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SpringbootActuatorApplicationTests {

    @Test
    void contextLoads() {
    }

}

2. JUnit 5 common notes and tests

JUnit 5 annotations are different from JUnit 4 annotations

JUnit 5 User Guide

  • @Test: indicates that the method is a test method. However, unlike JUnit4's @ test, it has a very single responsibility and cannot declare any attributes. Jupiter will provide additional tests for expanded tests
  • @ParameterizedTest: indicates that the method is parameterized test, which will be described in detail below
  • @RepeatedTest: indicates that the method can be executed repeatedly, which will be described in detail below
  • @DisplayName: set the display name for the test class or test method
  • @Before each: indicates to execute before each unit test
  • @After each: means to execute after each unit test
  • @Before all: indicates to execute before all unit tests
  • @After all: means to execute after all unit tests
  • @Tag: indicates the unit test category, similar to @ Categories in JUnit4
  • @Disabled: indicates that the test class or test method is not executed, similar to @ Ignore in JUnit 4
  • @Timeout: indicates that the test method will return an error if it exceeds the specified time
  • @ExtendWith: provides an extension class reference for a test class or test method

2.1 @DisplayName,@Disabled,@BeforeEach,@AfterEach,@BeforeAll,@AfterAll

package com.szh.boot;

import org.junit.jupiter.api.*;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

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

/**
 *
 */
//@SpringBootTest
@DisplayName("Junit5 Functional test class")
public class TestJunit {

    //----------------------------Common notes----------------------------
    @DisplayName("test DisplayName annotation---Method 1")
    @Test
    public void testDisplayName1() {
        System.out.println(123);
    }

    @Disabled
    @DisplayName("test DisplayName annotation---Method 2")
    @Test
    public void testDisplayName2() {
        System.out.println(456);
    }

    @BeforeEach
    public void testBeforeEach() {
        System.out.println("The test is about to begin....");
    }

    @AfterEach
    public void testAfterEach() {
        System.out.println("The test is over....");
    }

    @BeforeAll
    public static void testBeforeAll() {
        System.out.println("All tests are about to begin....");
    }

    @AfterAll
    public static void testAfterAll() {
        System.out.println("All tests are over....");
    }

}

Let's open the @ Disabled annotation on the testDisplayName2 method. As you can see, this test method has been Disabled.

2.2 @Timeout

package com.szh.boot;

import org.junit.jupiter.api.*;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

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

/**
 *
 */
//@SpringBootTest
@DisplayName("Junit5 Functional test class")
public class TestJunit {

    @BeforeEach
    public void testBeforeEach() {
        System.out.println("The test is about to begin....");
    }

    @AfterEach
    public void testAfterEach() {
        System.out.println("The test is over....");
    }

    @BeforeAll
    public static void testBeforeAll() {
        System.out.println("All tests are about to begin....");
    }

    @AfterAll
    public static void testAfterAll() {
        System.out.println("All tests are over....");
    }

    @DisplayName("test Timeout annotation")
    @Timeout(value = 5, unit = TimeUnit.MILLISECONDS)
    @Test
    public void testTimeout() throws InterruptedException {
        Thread.sleep(1000);
    }

}

2.3 @RepeatedTest

package com.szh.boot;

import org.junit.jupiter.api.*;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

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

/**
 *
 */
//@SpringBootTest
@DisplayName("Junit5 Functional test class")
public class TestJunit {

    @BeforeEach
    public void testBeforeEach() {
        System.out.println("The test is about to begin....");
    }

    @AfterEach
    public void testAfterEach() {
        System.out.println("The test is over....");
    }

    @BeforeAll
    public static void testBeforeAll() {
        System.out.println("All tests are about to begin....");
    }

    @AfterAll
    public static void testAfterAll() {
        System.out.println("All tests are over....");
    }

    @RepeatedTest(5)
    @Test
    public void testRepeatedTest() {
        System.out.println(1);
    }

}

3. Assertion

Assertions are the core part of the test method, which is used to verify the conditions that the test needs to meet. These assertion methods are static methods of org.junit.jupiter.api.Assertions. The built-in assertions in JUnit 5 can be divided into the following categories:

Check whether the data returned by the business logic is reasonable.

After all test runs are completed, there will be a detailed test report;

3.1 simple assertion

Used for simple validation of a single value. For example:

method

explain

assertEquals

Determines whether two objects or two primitive types are equal

assertNotEquals

Determines whether two objects or two primitive types are not equal

assertSame

Judge whether two object references point to the same object

assertNotSame

Judge whether two object references point to different objects

assertTrue

Determines whether the given Boolean value is true

assertFalse

Determines whether the given Boolean value is false

assertNull

Determines whether the given object reference is null

assertNotNull

Determines whether the given object reference is not null

package com.szh.boot;

import org.junit.jupiter.api.*;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

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

/**
 *
 */
//@SpringBootTest
@DisplayName("Junit5 Functional test class")
public class TestJunit {

    @BeforeEach
    public void testBeforeEach() {
        System.out.println("The test is about to begin....");
    }

    @AfterEach
    public void testAfterEach() {
        System.out.println("The test is over....");
    }

    @BeforeAll
    public static void testBeforeAll() {
        System.out.println("All tests are about to begin....");
    }

    @AfterAll
    public static void testAfterAll() {
        System.out.println("All tests are over....");
    }

    //----------------------------Assert----------------------------

    /**
     * If the previous assertion fails, the subsequent code will no longer execute
     */
    @DisplayName("Test simple assertions")
    @Test
    public void testSimpleAssertions() {
        int result = cal(1,2);
        assertEquals(5,result,"Business logic calculation result error....");
        Object obj1 = new Object();
        Object obj2 = new Object();
        assertSame(obj1,obj2);
    }

    public int cal(int x, int y) {
        return x + y;
    }

}

The assertEquals method judges that the operation results are different, that is, if the first assertion fails, the subsequent assertSame will not be executed.

When we modify the assertEquals method to make the assertion successful, the subsequent assertSame will continue to execute.

What you see at this time is the exception information printed by the assertSame method (the two objects are not equal).

assertEquals(3,result,"Business logic calculation result error....");

3.2 array assertion

Use the assertArrayEquals method to determine whether two objects or arrays of original types are equal.

package com.szh.boot;

import org.junit.jupiter.api.*;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

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

/**
 *
 */
//@SpringBootTest
@DisplayName("Junit5 Functional test class")
public class TestJunit {

    @BeforeEach
    public void testBeforeEach() {
        System.out.println("The test is about to begin....");
    }

    @AfterEach
    public void testAfterEach() {
        System.out.println("The test is over....");
    }

    @BeforeAll
    public static void testBeforeAll() {
        System.out.println("All tests are about to begin....");
    }

    @AfterAll
    public static void testAfterAll() {
        System.out.println("All tests are over....");
    }

    //----------------------------Assert----------------------------
    @DisplayName("Test array assertion 1")
    @Test
    public void testArrayAssertions1() {
        assertArrayEquals(new int[]{1,2}, new int[]{1,2});
    }

    @DisplayName("Test array assertion 2")
    @Test
    public void testArrayAssertions2() {
        assertArrayEquals(new int[]{3,4}, new int[]{1,2}, "Array contents are not equal....");
    }

}

3.3 combined assertions

The assertAll method accepts multiple instances of the org.junit.jupiter.api.Executable functional interface as assertions to be verified. These assertions can be easily provided through lambda expressions.

package com.szh.boot;

import org.junit.jupiter.api.*;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

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

/**
 *
 */
//@SpringBootTest
@DisplayName("Junit5 Functional test class")
public class TestJunit {

    @BeforeEach
    public void testBeforeEach() {
        System.out.println("The test is about to begin....");
    }

    @AfterEach
    public void testAfterEach() {
        System.out.println("The test is over....");
    }

    @BeforeAll
    public static void testBeforeAll() {
        System.out.println("All tests are about to begin....");
    }

    @AfterAll
    public static void testAfterAll() {
        System.out.println("All tests are over....");
    }

    //----------------------------Assert----------------------------
    @DisplayName(("Test combination assertion"))
    @Test
    public void testAllAssertions() {
        assertAll("testAll",
                () -> assertTrue(true && true),
                () -> assertEquals(1,2));
        //When the above two assertions are all successful, the following content will be printed
        System.out.println("success!!!");
    }

}

In composite assertions, when all assertions are successful, the code will execute down smoothly. Although assertTrue was executed successfully, assertEquals failed. All the following South will not be executed.

3.4 exception assertion

In JUnit 4, when you want to test the exception of a method, the ExpectedException variable annotated with @ Rule is still troublesome. JUnit 5 provides a new assertion method, Assertions.assertThrows(), which can be used in conjunction with functional programming.

package com.szh.boot;

import org.junit.jupiter.api.*;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

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

/**
 *
 */
//@SpringBootTest
@DisplayName("Junit5 Functional test class")
public class TestJunit {

    @BeforeEach
    public void testBeforeEach() {
        System.out.println("The test is about to begin....");
    }

    @AfterEach
    public void testAfterEach() {
        System.out.println("The test is over....");
    }

    @BeforeAll
    public static void testBeforeAll() {
        System.out.println("All tests are about to begin....");
    }

    @AfterAll
    public static void testAfterAll() {
        System.out.println("All tests are over....");
    }


    //----------------------------Assert----------------------------
    @DisplayName("Test exception assertion")
    @Test
    public void testExceptionAssertions() {
        assertThrows(ArithmeticException.class,() -> {
            int i = 10 / 2; //The assertion is executed
            //int j = 10 / 0; // The assertion is not executed at this time
        },"Business logic is running normally???");
    }

}

If the code execution int i = 10 / 2 runs correctly, no arithmetexception exception will occur, so the assertThrows assertion will be executed at this time. Otherwise, it will not be executed.

3.5 timeout assertion

Junit5 also provides Assertions.assertTimeout() to set the timeout for the test method.

package com.szh.boot;

import org.junit.jupiter.api.*;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

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

/**
 *
 */
//@SpringBootTest
@DisplayName("Junit5 Functional test class")
public class TestJunit {

    @BeforeEach
    public void testBeforeEach() {
        System.out.println("The test is about to begin....");
    }

    @AfterEach
    public void testAfterEach() {
        System.out.println("The test is over....");
    }

    @BeforeAll
    public static void testBeforeAll() {
        System.out.println("All tests are about to begin....");
    }

    @AfterAll
    public static void testAfterAll() {
        System.out.println("All tests are over....");
    }

    @DisplayName("Test timeout assertion")
    @Test
    public void testTimeoutAssertions() {
        assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(2000));
    }

}

3.6 rapid failure

The test fails directly through the fail method.

package com.szh.boot;

import org.junit.jupiter.api.*;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

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

/**
 *
 */
//@SpringBootTest
@DisplayName("Junit5 Functional test class")
public class TestJunit {

    @BeforeEach
    public void testBeforeEach() {
        System.out.println("The test is about to begin....");
    }

    @AfterEach
    public void testAfterEach() {
        System.out.println("The test is over....");
    }

    @BeforeAll
    public static void testBeforeAll() {
        System.out.println("All tests are about to begin....");
    }

    @AfterAll
    public static void testAfterAll() {
        System.out.println("All tests are over....");
    }

    @DisplayName("Rapid failure")
    @Test
    public void testFailAssertions() {
        System.out.println(1);
        System.out.println(2);
        fail("Go straight....");
        System.out.println(3);
        System.out.println(4);
    }

}

4. Preconditions

Preconditions in JUnit 5 are similar to assertions. The difference is that unsatisfied assertions will fail the test method, while unsatisfied preconditions will only terminate the execution of the test method. Preconditions can be regarded as the premise of test method execution. When the premise is not met, there is no need to continue execution.

package com.szh.boot;

import org.junit.jupiter.api.*;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

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

/**
 *
 */
//@SpringBootTest
@DisplayName("Junit5 Functional test class")
public class TestJunit {

    @BeforeEach
    public void testBeforeEach() {
        System.out.println("The test is about to begin....");
    }

    @AfterEach
    public void testAfterEach() {
        System.out.println("The test is over....");
    }

    @BeforeAll
    public static void testBeforeAll() {
        System.out.println("All tests are about to begin....");
    }

    @AfterAll
    public static void testAfterAll() {
        System.out.println("All tests are over....");
    }

    //----------------------------Preconditions----------------------------
    @DisplayName("Test preconditions")
    @Test
    public void testAssumptions() {
        Assumptions.assumeTrue(true, "The result is not true....");
        System.out.println(111);
    }

}

Set Assumptions.assumeTrue(false, "the result is not true..."); Change true in to false because true= False, so if the precondition is not met, the execution of the test method will be terminated directly.

5. Nested test

JUnit 5 can implement Nested tests through internal classes and @ Nested annotations in Java, so as to better organize relevant test methods together. You can use @ BeforeEach and @ AfterEach annotations in internal classes, and there is no limit to the nesting level.

package com.szh.boot;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import java.util.EmptyStackException;
import java.util.Stack;

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

/**
 * In nested tests, the outer @ Test cannot drive the inner @ Before(After)Each/All and other methods to run in advance / after
 *          The inner @ Test can drive the outer @ Before(After)Each/All and other methods to run in advance / later
 */
@DisplayName("A stack")
public class TestingAStackDemo {

    Stack<Object> stack;

    @Test
    @DisplayName("is instantiated with new Stack()")
    void isInstantiatedWithNew() {
        new Stack<>();
    }

    @Nested
    @DisplayName("when new")
    class WhenNew {

        @BeforeEach
        void createNewStack() {
            stack = new Stack<>();
        }

        @Test
        @DisplayName("is empty")
        void isEmpty() {
            assertTrue(stack.isEmpty());
        }

        @Test
        @DisplayName("throws EmptyStackException when popped")
        void throwsExceptionWhenPopped() {
            assertThrows(EmptyStackException.class, stack::pop);
        }

        @Test
        @DisplayName("throws EmptyStackException when peeked")
        void throwsExceptionWhenPeeked() {
            assertThrows(EmptyStackException.class, stack::peek);
        }

        @Nested
        @DisplayName("after pushing an element")
        class AfterPushing {
            String anElement = "an element";

            @BeforeEach
            void pushAnElement() {
                stack.push(anElement);
            }

            @Test
            @DisplayName("it is no longer empty")
            void isNotEmpty() {
                assertFalse(stack.isEmpty());
            }

            @Test
            @DisplayName("returns the element when popped and is empty")
            void returnElementWhenPopped() {
                assertEquals(anElement, stack.pop());
                assertTrue(stack.isEmpty());
            }

            @Test
            @DisplayName("returns the element when peeked but remains not empty")
            void returnElementWhenPeeked() {
                assertEquals(anElement, stack.peek());
                assertFalse(stack.isEmpty());
            }
        }
    }
}

6. Parametric test

Parametric testing is a very important new feature of JUnit 5. It makes it possible to run tests multiple times with different parameters, and also brings a lot of convenience to our unit testing.

Using @ ValueSource and other annotations to specify input parameters, we can use different parameters for multiple unit tests without adding a unit test every time a parameter is added, saving a lot of redundant code.

@ValueSource: Specifies the input parameter source for parametric testing. It supports eight basic classes, String type and Class type

@NullSource: indicates that a null input parameter is provided for the parameterized test

@EnumSource: indicates that an enumeration input parameter is provided for the parameterized test

@CsvFileSource: read the contents of the specified CSV file as the parametric test input parameter

@MethodSource: means to read the return value of the specified method as the parameterized test input parameter (note that the method return needs to be a stream)

7.JUnit4 → Junit5

https://junit.org/junit5/docs/current/user-guide/#migrating-from-junit4

The following changes should be noted during migration:

  • Annotations are in the org.junit.jupiter.api package, assertions are in the org.junit.jupiter.api.Assertions class, and preconditions are in the org.junit.jupiter.api.Assumptions class.
  • Replace @ Before and @ After with @ BeforeEach and @ AfterEach.
  • Replace @ BeforeClass and @ AfterClass with @ BeforeAll and @ AfterAll.
  • Replace @ Ignore with @ Disabled.
  • Replace @ Category with @ Tag.
  • Replace @ RunWith, @ Rule and @ ClassRule with @ ExtendWith.

Keywords: Junit Spring Boot unit testing

Added by Tucker1337 on Fri, 12 Nov 2021 11:25:08 +0200