preface

Here comes the first article of the Nuggets, hope to keep updating. The reason for writing this article is that a few months ago, I saw the unit test written by my colleague in the next group and was amazed instantly. The first time I realized that unit tests could be written this way, compared to the pile of tests I wrote before, it was disgusting to use myself. So I took a good look at the JUnit5 that their project team was using, summarized some new features, and hopefully wrote some amazing code myself someday.

JUnit5 introduction

JUnit has been the most popular unit testing framework in the Java space for decades. JUnit5 was finally released in 2017, three years after the release of JUnit4.

As the latest version of the JUnit framework, JUnit5 is quite different from previous versions. First, Junit5 consists of several different modules from three different subprojects.

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

JUnit Platform: The JUnit Platform is the foundation for launching the testing framework on the JVM. It supports not only JUnit’s own test engine, but also other test engines.

JUnit Jupiter: JUnit Jupiter provides a new programming model for JUnit5 and is at the heart of new features in JUnit5. A test engine is included for running on the Junit Platform.

JUnit Vintage: Since JUint has been in development for many years, JUnit Vintage provides JUnit4.x, JUnit 3.x compatible test engines in order to take care of older projects.

From the above introduction, it seems that JUint5 is no longer content with quietly being a unit testing framework. It wants to become a unit testing platform by connecting different test engines to support the use of various test frameworks. Therefore, it also adopts a layered architecture, divided into platform layer, engine layer, framework layer. The picture below shows this clearly:

As long as JUnit’s test engine interface is implemented, any test framework can run on JUnit Platform, which means JUnit5 will be very extensible.

Start a JUnit5 project!

The introduction of JUnit5 was relatively complex compared to the previous testing framework, requiring the introduction of jar packages for three modules. Since the project is currently built by Gradle, the following configurations are gradle configurations.

    testCompile("Org. Junit. Platform: junit - platform - the launcher: 1.6.0." ")
    testCompile("Org. Junit. Jupiter: junit - Jupiter - engine: 5.6.0")
    testCompile("Org. Junit. Vintage: junit - vintage - engine: 5.6.0")
Copy the code

Of course, you might think it’s too complicated to import so many packages, but that’s ok, the latest version of Junit5 takes that into account. Now all you need to do is introduce the following package

   testCompile("Org. Junit. Jupiter: junit - Jupiter: 5.6.0")
Copy the code

Oh, right. Don’t forget that our JUnit5 runs on JUnit Platform, so we need to add this to build.gradle as well. (All are trample pit….)

test {
    useJUnitPlatform()
}
Copy the code

With JUnit5 introduced, you are ready to start your first unit test. Note * * * * @ Test annotation using the org. Junit. Jupiter. API. The Test package, don’t use into takeup version.

import org.junit.jupiter.api.Test; // Note the use of Jupiter's Test annotation!!


public class TestDemo {

  @Test
  @displayName (" First test ")
  public void firstTest(a) {
      System.out.println("hello world");
  }

Copy the code

The basic annotation

The JUnit5 annotations are different from the JUnit4 annotations, and the ones listed below are some of the ones I find most common

annotations instructions
@Test The presentation method is the test method. But unlike JUnit4’s @test, whose responsibilities are very simple and cannot declare any attributes, extended tests will be provided by Jupiter with additional tests
@ParameterizedTest The method is parameterized test, which will be described in detail below
@RepeatedTest The method can be repeated, as described below
@DisplayName Set the display name for the test class or test method
@BeforeEach Represents execution before each unit test
@AfterEach Indicates execution after each unit test
@BeforeAll Indicates execution before all unit tests
@AfterAll Indicates execution after all unit tests
@Tag Represents a unit test category, similar to @categories in JUnit4
@Disabled Indicates that the test class or test method is not executed, similar to @ignore in JUnit4
@Timeout Indicates that the test method will return an error if it runs longer than the specified time
@ExtendWith Provide extended class references for test classes or test methods

The new features

More powerful assertions

JUnit5 USES a new assertion classes: org. Junit. Jupiter. API. Assertions. There are many new features than the previous Assert assertion classes, and a number of methods support Java8 Lambda expressions.

Here are two different assertions from JUnit4:

1. Exception assertion

In the JUnit4 era, it was cumbersome to test the ExpectedException variable with the @rule annotation if you wanted to test the method for exceptions. JUnit5 provides a new assertion mode, always.assertthrows (), that can be used with functional programming.

@Test
@displayName (" exception test ")
public void exceptionTest(a) {
    ArithmeticException exception = Assertions.assertThrows(
           // Throw an assertion exception
            ArithmeticException.class, () -> System.out.println(1 % 0));

}
Copy the code

2. Timeout assertion

Junit5 also provides Assertions.assertTimeout() that sets a timeout for test methods

@Test
@displayName (" Timeout test ")
public void timeoutTest(a) {
    // An exception will occur if the test method takes longer than 1s
    Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
}
Copy the code

Parametric test

Parameterized testing is an important new feature of JUnit5, and one that I find most surprising to me. It makes it possible to run tests multiple times with different parameters, and it also brings a lot of convenience to our unit tests.

Basic usage

With annotations like @Valuesource, specifying input parameters, we will be able to unit test multiple times with different parameters instead of having to add a unit test for every new parameter, saving a lot of redundant code.

ValueSource: Specifies an input source for parameterized tests. Supports eight base classes as well as String and Class

NullSource: provides a null input parameter for parameterized tests

@enumSource: indicates providing an enumeration input parameter to parameterized tests

@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@displayName (" Parameterized test 1")
public void parameterizedTest1(String string) {
    System.out.println(string);
    Assertions.assertTrue(StringUtils.isNotBlank(string));
}
Copy the code

Advanced usage

Of course, if parameterized tests can only specify common input parameters, it will not reach the point that MAKES me feel amazing. What really struck me as his strength was his ability to support all kinds of outside involvement. For example, CSV,YML,JSON files and even the return value of a method can be used as input parameters. Just implement the ArgumentsProvider interface, and any external file can be used as an input.

@csvfilesource: reads the content of a specified CSV file as an input parameter to a parameterized test

@methodSource: reads the return value of the specified method as a parameterized test parameter (note that the method return needs to be a stream)

/** * CSV file contents: * Shawn,24 * uzi,50 */
@ParameterizedTest
@CsvFileSource(resources = "/test.csv")  // Specify the CSV file location
@displayName (" Parameterized test - CSV file ")
public void parameterizedTest2(String name, Integer age) {
    System.out.println("name:" + name + ",age:" + age);
    Assertions.assertNotNull(name);
    Assertions.assertNotNull(age);
}

@ParameterizedTest
@MethodSource("method")    // Specify the method name
@displayName (" method source argument ")
public void testWithExplicitLocalMethodSource(String name) {
    System.out.println(name);
    Assertions.assertNotNull(name);
}

static Stream<String> method(a) {
    return Stream.of("apple"."banana");
}
Copy the code

Why parameterized tests?

After introducing parameterized testing, you may feel that this feature is really powerful, but it doesn’t seem to have many application scenarios, after all, most of the unit tests we write will not be reused. There is one big advantage to reading in from an external file. Yes, it is decoupled, so that the test logic is decoupled from the test parameters. If the test parameters change in the future, we do not need to modify the test code, but only the corresponding file. Let our test logic not be disturbed by a lot of code to construct test parameters, can focus on writing good test logic.

We also currently have our own wrapped @Filesource annotations for reading input parameters from external Json or Yaml files. The logic of the unit test code will be clear. Using this approach makes the logic of the test code much clearer than if you had to mock out many of the test parameters yourself.

@displayName (" Create advisory source: successfully created ")
@ParameterizedTest
@FileSource(resources = "testData/consultconfig_app_service.yaml")
public void testSaveConsultSource_SUCCESS(ConsultConfigAppServiceDTO dto) {
    // Get the input parameter
    ConsultSourceSaveDTO saveDTO = dto.getSaveConsultSourceSuccess();
    // Call the test method
    ConsultSourceBO consultSourceBO = consultConfigAppService.saveConsultSource(saveDTO);
    / / verification
    Assertions.assertEquals("testConsultSourceCmd", consultSourceBO.getConsultChannelName());
}
Copy the code

Embedded unit tests

JUnit5 provides nested unit tests to better represent relationships between individual unit test classes. When we write unit tests, we usually write unit tests one class at a time. However, for classes that have business relationships with each other, their unit tests can be written together and expressed in an inline manner, reducing the number of test classes and preventing class explosion.

JUnit 5 provides the @nested annotation to logically group test case classes in the form of static inner member classes

public class NestedTestDemo {

    @Test
    @DisplayName("Nested")
    void isInstantiatedWithNew(a) {
        System.out.println("Top Layer -- Embedded unit Tests");
    }

    @Nested
    @DisplayName("Nested2")
    class Nested2 {

        @BeforeEach
        void Nested2_init(a) {
            System.out.println("Nested2_init");
        }

        @Test
        void Nested2_test(a) {
            System.out.println("Layer 2 - Embedded unit Tests");
        }


        @Nested
        @DisplayName("Nested3")
        class Nested3 {

            @BeforeEach
            void Nested3_init(a) {
                System.out.println("Nested3_init");
            }

            @Test
            void Nested3_test(a) {
                System.out.println("Layer 3 - Embedded unit Tests"); }}}}Copy the code

Repeat the test

JUnit5 provides the @REPEATedTest annotation to allow a unit test to be executed multiple times. I don’t really understand why a unit test should be run more than once. At present, my personal understanding is that unit tests need to be repeatable, and running them multiple times can ensure the accuracy of the tests and prevent some randomness.

@RepeatedTest(10) // Repeat the command for 10 times
@displayName (" Repeat test ")
public void testRepeated(a) {
    Assertions.assertTrue(1= =1);
}
Copy the code

Dynamic testing

JUnit5 allows us to create unit tests dynamically, using the @TestFactory annotation, which generates unit tests at run time. Note that the @TestFactory modified method is not itself a unit test; it is only responsible for generating unit tests. We only need to return an iterator or even a stream of dynamic tests to generate different unit tests.

(Dynamic testing application scenarios I feel less, if you have thought of it, please add)

@TestFactory
@displayName (" dynamic test ")
Iterator<DynamicTest> dynamicTests(a) {
    return Arrays.asList(
            dynamicTest("First dynamic test.", () -> assertTrue(true)),
            dynamicTest("Second dynamic test", () -> assertEquals(4.2 * 2))
    ).iterator();
}
Copy the code

conclusion

That’s the basic introduction to JUnit5. Unit test is a very important link in software development. Good unit test can greatly increase the quality and maintainability of the system. JUnit5 has a lot of new features for writing unit tests. If your project is still using JUnit4, why not give it a try and open the door to a new world?

The resources

JUnit 5 User Guide

How many of the new Junit5 features have you used?