preface

Unit testing is an indispensable part of software development, but it is often ignored in common development because of tight project cycle and heavy workload, which often leads to endless software problems. Many online problems can be found and dealt with in time under the condition of unit tests, so it is necessary to cultivate the ability to write unit tests in daily development. This article introduces the basics of JUnit 5, the Java unit testing framework, and how to use it to write unit tests. Hopefully, it will also be helpful for you.

All of the code snippets covered in this article are in the following repository, interested partners are welcome to refer to:

Github.com/wrcj12138aa…

Version support:

  • JDK 8
  • JUnit 5.5.2
  • Lomok 1.18.8

Know JUnit 5

JUnit has a long history and a rich and evolving set of features. JUnit, along with TestNG, is the main market for unit testing frameworks in the Java space. Favored by most Java developers.

As for JUnit’s history, JUnit originated in 1997 when two programming gurus, Kent Beck and Erich Gamma, were on an airplane trip. Due to the lack of mature tools for Java testing at that time, The two collaborated on the prototype of JUnit, designed to be a better Java testing framework, while on the plane. Now, more than two decades later, JUnit has evolved through iterations to 5.x, providing better support (such as support for Lambda) and richer forms of testing (such as repeated testing, parameterized testing) on JDK 8 and beyond.

With JUint in mind, JUnit 5 is a major upgrade to the JUnit unit testing framework. It requires a Java 8 + runtime environment, which can be compiled and run on older JDK versions, but fully uses JUnit 5 functionality. The JDK 8 environment is a must.

In addition, JUnit 5 differs from previous versions of JUnit in that it is broken down into several different modules consisting of three different subprojects.

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

  • JUnit Platform: The basic service for starting test frameworks on the JVM, providing support for executing tests from the command line, IDE, and build tools.

  • JUnit Jupiter: Contains JUnit 5’s new programming model and extension model, mainly for writing test code and extension code.

  • JUnit Vintage: For test cases that are compatible with running JUnit3.x and JUnit4.x in JUnit 5.

Based on this introduction, you can see the following figure to get an idea of JUnit 5’s architecture and modules:

Why do you need JUnit 5

Having said what JUnit 5 is, let’s ask ourselves: Why do we need a JUnit 5?

Since the Java unit testing field has matured with testing frameworks like JUnit, developers have become increasingly demanding of unit testing frameworks: more ways to test, and less dependent on other libraries. As a result, a more powerful testing framework is expected. JUnit, the leader in Java testing, has released JUnit 5.

  • Provides new assertions and test annotations that support embedding of test classes

  • Richer testing methods: support dynamic testing, repeated testing, parametric testing and so on

  • Modularity is implemented to decoupled different modules such as test execution and test discovery, reducing dependencies

  • Provides support for Java 8, such as Lambda expressions, Sream API, etc.

This section describes common usage of JUnit 5

Next, let’s look at some common uses of JUni 5 to help us get up to speed on how to use JUnit 5.

First, introduce JUnit 5 dependency coordinates in the Maven project. Note that the current JDK environment must be above Java 8.

<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter-engine</artifactId>
  <version>5.5.2</version>
  <scope>test</scope>
</dependency>
Copy the code

The first test case

To introduce JUnit 5, we can quickly write a simple test case to get a glimpse of JUnit 5:

@DisplayName("My first test case.")
public class MyFirstTestCaseTest {

    @BeforeAll
    public static void init(a) {
        System.out.println("Initialize data");
    }

    @AfterAll
    public static void cleanup(a) {
        System.out.println("Clean up data");
    }

    @BeforeEach
    public void tearup(a) {
        System.out.println("Start of current test method");
    }

    @AfterEach
    public void tearDown(a) {
        System.out.println("Current test method ends");
    }

    @DisplayName("My first test")
    @Test
    void testFirstTest(a) {
        System.out.println("My first test begins testing.");
    }

    @DisplayName("My second test.")
    @Test
    void testSecondTest(a) {
        System.out.println("My second test begins testing."); }}Copy the code

Run the test case directly and you can see the console log as follows:

You can see in the left column that the test item name is the name we set on the test class and method using @displayname. This annotation is introduced by JUnit 5 to define a test class and specify the DisplayName of the use case in the test report. This annotation can be used on classes and methods. Using it on a class means it is a test class, and using it on a method means it is a test method.

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@API(status = STABLE, since = "5.0")
public @interface DisplayName {
	String value(a);
}
Copy the code

Take a look at the pair of annotations ** @beforeAll ** and @AfterAll used in the sample code. They define the actions of the entire test class before and at the end. They can only be used to decorate static methods and are mainly used to initialize and clean up global data and external resources needed during the test. In contrast, the @beforeeach and @Aftereach annotated methods are executed before and at the end of each test case method and are primarily responsible for preparing and destroying the environment that the test case needs to run.

In addition to these basic annotations, there are many more rich and powerful annotations in the testing process, so let’s learn about them one by one.

To disable performing tests: @disabled

When we want to run a test class, skip a test method, and run other test cases normally, we can use the @disabled annotation to indicate that the test method is not available and that JUnit will not execute the test method that executes the test class.

Take a look at @disbaled in action, adding the following code to the original test class:

@DisplayName("My third test.")
@Disabled
@Test
void testThirdTest(a) {
	System.out.println("My third test begins testing.");
}
Copy the code

After running the command, you will see the following console log: methods marked @disabled are not executed, only separate method information is printed:

@disabled can also be used on a class to mark that all test methods under that class will not be executed. This is usually used when testing multiple test classes in combination.

Nested test class: @Nested

As more and more classes and code are written, more and more test classes need to be tested. To address the exploding number of test classes, JUnit 5 provides the @Nested annotation, which logically groups test case classes in the form of static inner member classes. And each static inner class can have its own lifecycle methods, which are executed from the outer to the inner hierarchy. In addition, nested classes can also be marked with @displayName so that we can use the correct test name. Here’s a simple way to use it:

@DisplayName("Embedded test class")
public class NestUnitTest {
    @BeforeEach
    void init(a) {
        System.out.println("Preparation for test method execution");
    }

    @Nested
    @DisplayName("The first embedded test class")
    class FirstNestTest {
        @Test
        void test(a) {
            System.out.println("The first embedded test class executes the test"); }}@Nested
    @DisplayName("Second embedded test class")
    class SecondNestTest {
        @Test
        void test(a) {
            System.out.println("The second embedded test class performs tests"); }}}Copy the code

After running all the test cases, you should see the following results in the console:

Repeatability test: @repeatedtest

In JUnit 5, support for setting the run times of test methods has been added, allowing test methods to be run repeatedly. When you want to run a test method N times, you can mark it with @repeatedTest as shown below:

@DisplayName("Repeat test")
@RepeatedTest(value = 3)
public void i_am_a_repeated_test(a) {
	System.out.println("Execute tests");
}
Copy the code

After running, the test method will be executed for 3 times. The running effect in IDEA is as shown in the figure below:

This is basic. We can also change the name of the repeatable test method by using the built-in variable @repeatedtest to use placeholder on its name attribute.

@DisplayName("Custom name repeat test")
@RepeatedTest(value = 3, name = "{displayName} {currentRepetition} times")
public void i_am_a_repeated_test_2(a) {
	System.out.println("Execute tests");
}
Copy the code

The currentRepetition variable in the @repeatedtest annotation represents the number of repetitions, the totalRepetitions variable represents the total number of repetitions, and the displayName variable represents the displayName of the test method, We can directly use these built-in variables to redefine the name of the test method repeat runtime.

The new assertion

On the Assertions API design, JUnit 5 were significantly improved, and make full use of the new features of Java 8, especially the Lambda expressions, finally provides a new assertion classes: org. JUnit. Jupiter. API. Assertions. Many assertion methods take Lambda expression arguments, and one advantage of using Lambda expressions on an assertion message is that it is evaluated latently, which can save some time and resources if message construction is expensive.

You can now also group multiple assertions within a method using the assertAll method as shown in the following example code:

@Test
void testGroupAssertions(a) {
    int[] numbers = {0.1.2.3.4};
    Assertions.assertAll("numbers",
            () -> Assertions.assertEquals(numbers[1].1),
            () -> Assertions.assertEquals(numbers[3].3),
            () -> Assertions.assertEquals(numbers[4].4)); }Copy the code

If any of the assertions in the group assertion fail, a MultipleFailuresError error is raised.

Timeout operation test: assertTimeoutPreemptively

JUnit 5 has the assertTimeout method, which provides extensive support for timeouts, when we want to test the execution time of a time-consuming method and don’t want the test method to wait indefinitely.

Suppose we want our test code to execute in less than a second, we can write the following test case:

@Test
@DisplayName("Timeout method testing")
void test_should_complete_in_one_second(a) {
  Assertions.assertTimeoutPreemptively(Duration.of(1, ChronoUnit.SECONDS), () -> Thread.sleep(2000));
}
Copy the code

This test run fails because the code execution will sleep for two seconds and we expect the test case to succeed in less than one second. However, if we set the sleep time to one second, the test will still occasionally fail because the execution of the test method will take time to execute additional code and instructions in addition to the object code, so there is no exact match for the time parameter on the timeout limit.

Exception test: assertThrows

In our code, we usually use a try-catch method to handle methods with exceptions. For testing code that throws exceptions like this, JUnit 5 provides Assertions#assertThrows(Class

, Executable) for testing. The first argument is an exception type and the second is a functional interface argument, similar to the Runnable interface, that requires no arguments and does not return. And support the use of Lambda expressions, specific use can refer to the following code:

@Test
@DisplayName("Exceptions caught by tests")
void assertThrowsException(a) {
  String str = null;
  Assertions.assertThrows(IllegalArgumentException.class, () -> {
    Integer.valueOf(str);
  });
}
Copy the code

When the exception of Lambda expressions in the code could be exception types are compared with the first parameter, if you don’t belong to the same kind of exception, will console output as follows similar tip: org. Opentest4j. An AssertionFailedError is generated: Unexpected exception type thrown ==> expected:

but was: <… Exception>

JUnit 5 parameterized tests

To use JUnit 5 for parameterized tests, in addition to the junit-jupiter-engine base dependency, you need another module dependency: junit-jupiter-params, which primarily provides an API for writing parameterized tests. In the same way, the corresponding dependencies of the same version are introduced into the Maven project:

<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter-params</artifactId>
  <version>5.5.2</version>
  <scope>test</scope>
</dependency>
Copy the code

Basic data source test: @Valuesource

ValueSource is the simplest source of data parameters provided by JUnit 5. It supports the eight basic Java types and strings, Class, which are assigned to the corresponding type attribute on the annotation and passed as an array. The example code is as follows:

public class ParameterizedUnitTest {
    @ParameterizedTest
    @ValueSource(ints = {2.4.8})
    void testNumberShouldBeEven(int num) {
        Assertions.assertEquals(0, num % 2);
    }

    @ParameterizedTest
    @ValueSource(strings = {"Effective Java"."Code Complete"."Clean Code"})
    void testPrintTitle(String title) { System.out.println(title); }}Copy the code

The @parameterizedTest annotation replaces the @Test annotation as a necessary annotation for parameterized tests. Any parameterized test method needs to be marked with this annotation.

When you run the tests, as shown in the figure below, the target method is run for each parameter in @Valuesource, and when a parameter fails to run the test, that test method fails.

CSV data source test: @csvSource

@csvSource allows you to inject comma-separated values into a set of data in the specified CSV format, using each comma-separated value to match the parameters of a test method. Here is an example:

@ParameterizedTest
@CsvSource({"1,One"."2,Two"."3,Three"})
void testDataFromCsv(long id, String name) {
	System.out.printf("id: %d, name: %s", id, name);
}
Copy the code

In addition to using commas to separate the arguments, @csvSource supports custom symbols, just modify its Delimiter, which defaults to,.

JUnit also provides a way to read data from external CSV files as a data source implementation. You can simply specify the path to the resource file with @csvFilesource, which is as easy to use as @csvSource, which I won’t repeat here.

The @csvfilesource resource file path starts with/to find files in the current test resource directory.

In addition to the three data sources mentioned above, JUnit provides the following three data sources:

  • @enumsource: allows us to construct a specific value in the Enum Enum type by passing in the parameter values.
  • @methodSource: Specifies a method that returns Stream/Array/iterable as the data source. Note that the method must be static and cannot accept any arguments.
  • @ArgumentSource: Overrides it by implementing the argument classes of the ArgumentsProvider interface as the data sourceprovideArgumentsMethods can return Stream<Arguments> of a custom type to be used as data needed to test the method.

Those interested in the above three data source annotations can refer to the ParameterizedUnitTest class of the sample project, which will not be introduced again and again here.

conclusion

JUnit 5 for you here, it must also have a basic understanding and grasp, it is said that unit testing is to improve software quality, improve efficiency of research and development, a necessary link, from 5 write unit tests with JUnit, cultivate the habit of writing test code, in the development of practice to improve its efficiency, to write the code more quality assurance.

Recommended reading

  • Master Spring Boot Profiles
  • How do I gracefully close a Spring Boot application
  • Need interface management you understand?
  • You must know Lombok in Java
  • The New Generation of Java microservices: Nacos

The resources

  • Junit.org/junit5/docs…

  • www.baeldung.com/junit-5

  • zhuanlan.zhihu.com/p/43902194

  • Dev. To/stealthmusi…

  • Java Unit Testing with JUnit 5