目录

JUnit 5

JUnit 5 由三个不同子项目中的几个不同模块组成。

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

  • JUnit Platform 是基于JVM的运行测试的基础框架在,它定义了开发运行在这个测试框架上的TestEngine API。此外该平台提供了一个控制台启动器,可以从命令行启动平台,可以为Gradle和 Maven构建插件,同时提供基于JUnit 4的Runner。
  • JUnit Jupiter 是在JUnit 5中编写测试和扩展的新编程模型和扩展模型的组合.Jupiter子项目提供了一个TestEngine在平台上运行基于Jupiter的测试。
  • JUnit Vintage 提供了一个TestEngine在平台上运行基于JUnit 3和JUnit 4的测试。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!-- https://mvnrepository.com/artifact/org.junit/junit-bom -->
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.junit</groupId>
      <artifactId>junit-bom</artifactId>
      <version>5.9.1</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
1
2
3
4
5
6
7
8
9
dependencies {
  // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter
  testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
  testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.2'
  testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
}
test {
  useJUnitPlatform()
}

编译依赖 junit-jupiter-api junit-jupiter-params 与 运行时依赖 junit-jupiter-engine

运行测试

1
gradle test
  • @Test 注解在方法上标记方法为测试方法,以便构建工具和 IDE 能够识别并执行它们。
  • JUnit 5 不再需要手动将测试类与测试方法为public,包可见的访问级别就足够了。

初始化与销毁 生命周期

你可能需要执行一些代码来在测试执行前后完成一些初始化或销毁的操作。在 JUnit 5 中,有4个注解你可能会用于如此工作:

  • @BeforeAll 只执行一次,执行时机是在所有测试和 @BeforeEach 注解方法之前。
  • @BeforeEach 在每个测试执行之前执行。
  • @AfterEach 在每个测试执行之后执行。
  • @AfterAll 只执行一次,执行时机是在所有测试和 @AfterEach 注解方法之后。

因为框架会为每个测试创建一个单独的实例,在 @BeforeAll/@AfterAll 方法执行时尚无任何测试实例诞生。因此,这两个方法必须定义为静态方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class tests {

    @BeforeEach
    void init() {
        // 每条用例开始前执行
        System.out.println("用例开始");
    }

    @AfterEach
    void tearDown(){
        // 每条用例结束时执行
        System.out.println("用例结束");
    }

    @BeforeAll
    static void initAll() {
        // 所有用例开始前执行
        System.out.println("开始");
    }

    @AfterAll
    static void tearDownAll() {
        // 所有用例结束时执行
        System.out.println("结束");
    }
}

JUnit5 Annotations

Test Class: 任何顶级类、static成员类或 @Nested 类至少包含一个测试方法。测试类不能是 abstract,也必须有一个构造函数。

Test Method: 任何使用 @Test, @RepeatedTest, @ParameterizedTest, @TestFactory, 或 @TestTemplate 直接注解(annotated)或元注解(meta-annotated)的实例方法

Lifecycle Method: 任何通过 @BeforeAll, @AfterAll, @BeforeEach, or @AfterEach 直接注解或元注解的方法

JUnit5提供了以下的注解来编写测试代码。

Annotations 描述
@BeforeEach 在方法上注解,在每个测试方法运行之前执行。
@AfterEach 在方法上注解,在每个测试方法运行之后执行
@BeforeAll 该注解方法会在所有测试方法之前运行,该方法必须是静态的。
@AfterAll 该注解方法会在所有测试方法之后运行,该方法必须是静态的。
@Test 用于将方法标记为测试方法
@DisplayName 用于为测试类或测试方法提供任何自定义显示名称
@Disable 用于禁用或忽略测试类或方法
@Nested 用于创建嵌套测试类
@Tag 用于测试发现或过滤的标签来标记测试方法或类
@TestFactory 标记一种方法是动态测试的测试工场

断言 Assertions

JUnit 5 断言更容易验证预期的测试结果是否与实际结果匹配。如果测试的任何断言失败,则测试将失败。类似地,如果一个测试的所有断言都通过,则该测试将通过。 JUnit 5 断言是 org.junit.jupiter.api.Assertions 类中的静态方法。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
class AssertionsDemo {

  @Test
  void standardAssertions() {
    assertEquals(2, 2);
    assertEquals(
      4,
      4,
      "The optional assertion message is now the last parameter."
    );
    assertTrue(
      2 == 2,
      () ->
        "Assertion messages can be lazily evaluated -- " +
        "to avoid constructing complex messages unnecessarily."
    );
  }

  @Test
  void personHasFirstName() {
    Person person = new Person("John", "Doe");
    assertNotNull(person.getFirstName());
  }

  @Test
  void groupedAssertions() {
    // In a grouped assertion all assertions are executed, and any
    // failures will be reported together.
    assertAll(
      "person",
      () -> assertEquals("John", person.getFirstName()),
      () -> assertEquals("Doe", person.getLastName())
    );
  }

  @Test
  void dependentAssertions() {
    // Within a code block, if an assertion fails the
    // subsequent code in the same block will be skipped.
    assertAll(
      "properties",
      () -> {
        String firstName = person.getFirstName();
        assertNotNull(firstName);

        // Executed only if the previous assertion is valid.
        assertAll(
          "first name",
          () -> assertTrue(firstName.startsWith("J")),
          () -> assertTrue(firstName.endsWith("n"))
        );
      },
      () -> {
        // Grouped assertion, so processed independently
        // of results of first name assertions.
        String lastName = person.getLastName();
        assertNotNull(lastName);

        // Executed only if the previous assertion is valid.
        assertAll(
          "last name",
          () -> assertTrue(lastName.startsWith("D")),
          () -> assertTrue(lastName.endsWith("e"))
        );
      }
    );
  }

  @Test
  void exceptionTesting() {
    Throwable exception = assertThrows(
      IllegalArgumentException.class,
      () -> {
        throw new IllegalArgumentException("a message");
      }
    );
    assertEquals("a message", exception.getMessage());
  }

  @Test
  void timeoutNotExceeded() {
    // The following assertion succeeds.
    assertTimeout(
      ofMinutes(2),
      () -> {
        // Perform task that takes less than 2 minutes.
      }
    );
  }

  @Test
  void timeoutNotExceededWithResult() {
    // The following assertion succeeds, and returns the supplied object.
    String actualResult = assertTimeout(
      ofMinutes(2),
      () -> {
        return "a result";
      }
    );
    assertEquals("a result", actualResult);
  }

  @Test
  void timeoutNotExceededWithMethod() {
    // The following assertion invokes a method reference and returns an object.
    String actualGreeting = assertTimeout(
      ofMinutes(2),
      AssertionsDemo::greeting
    );
    assertEquals("hello world!", actualGreeting);
  }

  @Test
  void timeoutExceeded() {
    // The following assertion fails with an error message similar to:
    // execution exceeded timeout of 10 ms by 91 ms
    assertTimeout(
      ofMillis(10),
      () -> {
        // Simulate task that takes more than 10 ms.
        Thread.sleep(100);
      }
    );
  }

  @Test
  void timeoutExceededWithPreemptiveTermination() {
    // The following assertion fails with an error message similar to:
    // execution timed out after 10 ms
    assertTimeoutPreemptively(
      ofMillis(10),
      () -> {
        // Simulate task that takes more than 10 ms.
        Thread.sleep(100);
      }
    );
  }

  private static String greeting() {
    return "hello world!";
  }
}

假设 Assumptions

Assumptions 类提供了静态方法来支持基于假设的条件测试执行。失败的假设导致测试被中止。无论何时继续执行给定的测试方法没有意义,通常使用假设。在测试报告中,这些测试将被标记为已通过。

JUnit 的 Jupiter 假设类有两个这样的方法:assumeFalse()assumeTrue()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class AppTest {
    @Test
    void testOnDev()
    {
        System.setProperty("ENV", "DEV");
        Assumptions.assumeTrue("DEV".equals(System.getProperty("ENV")), AppTest::message);
    }

    @Test
    void testOnProd()
    {
        System.setProperty("ENV", "PROD");
        Assumptions.assumeFalse("DEV".equals(System.getProperty("ENV")));
    }

    private static String message () {
        return "TEST Execution Failed :: ";
    }
}

高级匹配

Hamcrest

1
2
3
4
5
6
@Test
void listHasOneItem() {
  List<String> list = new ArrayList();
  list.add("Hello");
  assertThat(list, hasSize(1));
}

AssertJ

1
2
3
4
5
6
7
@Test
    void listHasPerson() {
        List<Person> people = new ArrayList<>();
        people.add(new Person("John", "Doe"));
        people.add(new Person("Jane", "Doe"));
        assertThat(people).extracting("firstName").contains("John");
    }

Truth

1
2
3
4
5
6
7
8
@Test
    void listHasItemsInOrder() {
        List<String> fruits = new ArrayList<>();
        fruits.add("Citron");
        fruits.add("Orange");
        fruits.add("Grapefruit");
        assertThat(fruits).containsExactly("Citron", "Grapefruit", "Orange").inOrder();
    }

Nested 测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
@DisplayName("A stack")
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());
            }
        }
    }
}

测试接口 与 默认方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@TestInstance(Lifecycle.PER_CLASS)
interface TestLifecycleLogger {

    static final Logger logger = Logger.getLogger(TestLifecycleLogger.class.getName());

    @BeforeAll
    default void beforeAllTests() {
        logger.info("Before all tests");
    }

    @AfterAll
    default void afterAllTests() {
        logger.info("After all tests");
    }

    @BeforeEach
    default void beforeEachTest(TestInfo testInfo) {
        logger.info(() -> String.format("About to execute [%s]",
            testInfo.getDisplayName()));
    }

    @AfterEach
    default void afterEachTest(TestInfo testInfo) {
        logger.info(() -> String.format("Finished executing [%s]",
            testInfo.getDisplayName()));
    }
}
class TestInterfaceDemo implements TestLifecycleLogger {
    @Test
    void isEqualValue() {
        assertEquals(1, "a".length(), "is always equal");
    }
}

Parameterized Tests

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
    assertTrue(StringUtils.isPalindrome(candidate));
}
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = { " ", "   ", "\t", "\n" })
void nullEmptyAndBlankStrings(String text) {
    assertTrue(text == null || text.trim().isEmpty());
}
@ParameterizedTest
@EnumSource
void testWithEnumSourceWithAutoDetection(ChronoUnit unit) {
    assertNotNull(unit);
}
@ParameterizedTest
@EnumSource(names = { "DAYS", "HOURS" })
void testWithEnumSourceInclude(ChronoUnit unit) {
    assertTrue(EnumSet.of(ChronoUnit.DAYS, ChronoUnit.HOURS).contains(unit));
}

@ParameterizedTest
@MethodSource
void testWithExplicitLocalMethodSource(String argument) {
    assertNotNull(argument);
}
static Stream<String> testWithExplicitLocalMethodSource() {
    return Stream.of("apple", "banana");
}

@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultiArgMethodSource(String str, int num, List<String> list) {
    assertEquals(5, str.length());
    assertTrue(num >=1 && num <=2);
    assertEquals(2, list.size());
}
static Stream<Arguments> stringIntAndListProvider() {
    return Stream.of(
        arguments("apple", 1, Arrays.asList("a", "b")),
        arguments("lemon", 2, Arrays.asList("x", "y"))
    );
}

@ParameterizedTest
@CsvSource({
    "apple,         1",
    "banana,        2",
    "'lemon, lime', 0xF1",
    "strawberry,    700_000"
})
void testWithCsvSource(String fruit, int rank) {
    assertNotNull(fruit);
    assertNotEquals(0, rank);
}
// 自定义  display name
@DisplayName("Display name of container")
@ParameterizedTest(name = "{index} ==> the rank of ''{0}'' is {1}")
@CsvSource({ "apple, 1", "banana, 2", "'lemon, lime', 3" })
void testWithCustomDisplayNames(String fruit, int rank) {
}

@ParameterizedTest(name = "[{index}] {arguments}")
@CsvFileSource(files = "src/test/resources/two-column.csv", numLinesToSkip = 1)
void testWithCsvFileSourceFromFile(String country, int reference) {
    assertNotNull(country);
    assertNotEquals(0, reference);
}

two-column.csv

COUNTRY, REFERENCE
Sweden, 1
Poland, 2
"United States of America", 3
France, 700_000

自定义参数测试 - Json 文件

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void testWithArgumentsSource(String argument) {
    assertNotNull(argument);
}
public class MyArgumentsProvider implements ArgumentsProvider {

    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return Stream.of("apple", "banana").map(Arguments::of);
    }
}

@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ArgumentsSource(JsonFileArgumentsProvider.class)
public @interface JsonFileSource {
  String[] resources();

  String[] paths() default {};

  Class<?> clazz() default ObjectNode.class;
}
/**
 * @author Ynthm Wang
 * @version 1.0
 */
public class JsonFileArgumentsProvider
    implements ArgumentsProvider, AnnotationConsumer<JsonFileSource> {
  private final BiFunction<Class<?>, String, InputStream> inputStreamProvider;
  private JsonFileSource annotation;
  private String[] resources;
  private String[] paths;
  private Class<?> clazz;
  private ObjectMapper objectMapper;

  public JsonFileArgumentsProvider() {
    this(Class::getResourceAsStream);
  }

  public JsonFileArgumentsProvider(BiFunction<Class<?>, String, InputStream> inputStreamProvider) {
    this.inputStreamProvider = inputStreamProvider;
  }

  @Override
  public void accept(JsonFileSource annotation) {
    this.annotation = annotation;
    this.resources = annotation.resources();
    this.paths = annotation.paths();
    this.clazz = annotation.clazz();
    this.objectMapper = new ObjectMapper();
  }

  public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
    return Arrays.stream(this.resources)
        .map(
            (resource) -> {
              return this.openInputStream(context, resource);
            })
        .flatMap(this::toStream);
  }

  private InputStream openInputStream(ExtensionContext context, String resource) {
    Preconditions.notBlank(
        resource, "Classpath resource [" + resource + "] must not be null or blank");
    Class<?> testClass = context.getRequiredTestClass();
    return Preconditions.notNull(
        this.inputStreamProvider.apply(testClass, resource),
        () -> "Classpath resource [" + resource + "] does not exist");
  }

  private Stream<Arguments> toStream(InputStream inputStream) {
    try {
      JsonNode rootNode = objectMapper.readTree(inputStream);
      if (paths.length < 1) {
        if (rootNode.isArray()) {
          return ((List)
                  objectMapper.readValue(
                      rootNode.traverse(),
                      objectMapper.getTypeFactory().constructCollectionType(List.class, clazz)))
              .stream().map(i -> Arguments.of(i));
        } else {
          return Stream.of(Arguments.arguments(objectMapper.readValue(rootNode.traverse(), clazz)));
        }
      } else {
        for (String path : paths) {
          rootNode = rootNode.path(path);
        }
        // Spliterator 转 Stream
        if (rootNode.isArray()) {
          return StreamSupport.stream(
                  Spliterators.spliteratorUnknownSize(rootNode.elements(), Spliterator.ORDERED),
                  false)
              .map(
                  i -> {
                    try {
                      return Arguments.arguments(objectMapper.readValue(i.traverse(), clazz));
                    } catch (IOException e) {
                      throw new RuntimeException(e);
                    }
                  });
        }
        return Stream.of(Arguments.arguments(objectMapper.readValue(rootNode.traverse(), clazz)));
      }
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }
}

Running Unit Tests With Maven Plugin

maven-surefire-plugin 单元测试

  • Maven 通过 Maven Surefire Plugin 插件执行 单元测试。(通过Maven Failsafe Plugin插件执行集成测试
  • 在 pom.xml 中配置JUnit,TestNG测试框架的依赖,即可自动识别和运行src/test目录下利用该框架编写的测试用例
  • surefire也能识别和执行符合一定命名约定的普通类中的测试方法(POJO测试)。
  • 生命周期中test阶段默认绑定的插件目标就是surefire中的test目标,无需额外配置,直接运行mvn test就可以。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>2.22.2</version>
  <configuration>
   <!-- 跳过测试阶段-->
    <skipTests>true</skipTests>
   <!-- 忽略测试失败-->
    <testFailureIgnore>true</testFailureIgnore>
  </configuration>
</plugin>

maven-failsafe-plugin 集成测试

使用mock之后单元测试可以完全不依赖外界环境,比如database(一般使用hsqldb in memory db来实现database测试,mock db太麻烦了),ftp server,web service或者其他的功能模块。Mock测试带来的问题就是各个类,模块之间的集成测试完全没有做,这个时候就需要集成测试。单元测试maven有surefire插件实现自动化,集成测试则有failsafe plugin。

maven 的生命周期与集成测试相关的四个阶段

  1. pre-integration-test:该阶段用来准备集成测试环境,类似于 junit 单元测试中的 setUp
  2. integration-test:见名知意,该阶段执行集成测试
  3. post-integration-test:用来销毁集成测试的环境,类似于 junit 单元测试中的 tearDown
  4. verify :该阶段用于分析集成测试的结果
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<pluginManagement>
      <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-failsafe-plugin</artifactId>
          <version>2.22.2</version>
          <executions>
            <execution>
              <id>integration-test</id>
              <goals>
                <goal>integration-test</goal>
              </goals>
            </execution>
            <execution>
              <id>verify</id>
              <goals>
                <goal>verify</goal>
              </goals>
            </execution>
          </executions>
        </plugin>
      </plugins>
</pluginManagement>

高级主题

JUnit Platform 启动器

JUnit 5 引入了Launcher可用于发现、过滤和执行测试的概念。此外,第三方测试库(如 Spock、Cucumber 和 FitNesse)可以通过提供自定义的 TestEngine.

启动器 API 在junit-platform-launcher模块中。

启动器 API 的一个示例使用者是ConsoleLauncher项目中的 junit-platform-console。

JUnit Platform 套件引擎

JUnit 平台支持使用 JUnit 平台的任何测试引擎对测试套件的声明性定义和执行。

junit-platform-suite-api和junit-platform-suite-engine工件之外,您还需要至少一个其他测试引擎及其对类路径的依赖项。有关组 ID、工件 ID 和版本的详细信息

1
2
3
4
5
<dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-suite-engine</artifactId>
    <version>1.8.1</version>
</dependency>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Suite
@SuiteDisplayName("JUnit Platform Suite Demo")
@SelectPackages("com.ynthm.demo")
@IncludeClassNamePatterns(".*Tests")
class SuiteDemo {
}

@RunWith(JUnitPlatform.class)
@SelectClasses({AssertionTest.class, AssumptionTest.class, ExceptionTest.class})
public class AllUnitTest {}

附录