修改密码

JUnit 5 教程三分钟教程

+1

-1

收藏

+1

-1

点赞0

评论0

JUnit5 中支持通过 @Tag() 对测试用例进行分组提供 JUnit3 和 JUnit4 的测试引擎JUnit支持了些内建的转换在所有的 @Test @RepeatedTest @ParameterizedTest @TestFactory 之前执行@RepeatedTest() 执行多次测试定义测试用例的执行顺序测试的Class可以通过添加@DisplayName()3. 运行测试用例例如对比 [JUnit4](httpsJUnit5 提供了4个生命周期注解 @BeforeAll @

JUnit 5 作为新一代的 Java 单元测试框架,提供很多改进。例如对比 JUnit4JUnit5 的官网,JUnit5 的设计更加简约与时尚,至少不会抗拒阅读的程度了(像破烂一样的网站,看了整个人都难受,不影响效率?不存在的)
而且,除此外,他的文档使用了 Asciidoc, 相对于markdown复杂,主要是它还支持具有包含另一个文件内容,这对于写API文档来说挺重要的,有兴趣可以了解下~

Okay, 结束吐槽,让我来看看 JUnit5 到底带来了哪些变化吧

JUnit 5 是什么?

与以往的版本不同,JUnit5 由三个模块模版组成 JUnit Platform + JUnit Jupiter + JUnit Vintage

  • JUnit Platform:运行测试框架的基础服务,定义了一套API,任何实现这套API的测试引擎,都能运行在这之上
  • JUnit Jupiter:一系列用于编写JUnit5测试或者扩展的组合,同时他的子项目提供了JUnit5测试引擎
  • JUnit Vintage:提供 JUnit3 和 JUnit4 的测试引擎

三分钟教程

环境搭建

  1. 创建你的项目(建议Spring Boot),简单的勾选几个依赖
  2. 添加 JUnit5 的依赖(spring boot 2.2 中已默认是Junit5,不需要额外加,详见WIKI),
  1. <dependency>
  2. <groupId>org.junit.jupiter</groupId>
  3. <artifactId>junit-jupiter</artifactId>
  4. <version>${latest-version}</version>
  5. <scope>test</scope>
  6. </dependency>

org.junit.jupiter:junit-jupiter已包含了 JUnit Platform,不需要额外声明依赖,一个就够了

第一个测试用例

  1. 创建一个待测试的工具类

    public class TimeUtils {

    1. public static String hello(Instant now) {
    2. return "现在时间是:" + now.toString();
    3. }

    }

  1. 创建测试用例

    class TimeUtilsTest {

    1. @Test
    2. void hello() {
    3. Instant now = Instant.now();
    4. String expect = "现在时间是:" + now.toString();
    5. assertEquals(expect, TimeUtils.hello(now));
    6. }

    }

  1. 运行测试用例,如果你使用idea,那么直接点旁边的运行按钮,或者使用其它编辑器的功能测试,当然,你还可以选择通过命令行,下载junit-platform-console-standalone,并运行它(不懂),另一种是mvn test运行测试

更多实用方案

别名

测试的Class可以通过添加@DisplayName(),添加别名

  1. @DisplayName("时间工具类测试")
  2. class TimeUtilsTest {}

也可以使用@DisplayNameGeneration(),进行更多的配置

  1. @DisplayNameGeneration(TimeUtils2Test.ReplaceUnderscores.class)
  2. class TimeUtils2Test {
  3. @Test
  4. void hello() {
  5. Instant now = Instant.now();
  6. String expect = "现在时间是:" + now.toString();
  7. assertEquals(expect, TimeUtils.hello(now));
  8. }
  9. static class ReplaceUnderscores extends DisplayNameGenerator.ReplaceUnderscores {
  10. @Override
  11. public String generateDisplayNameForClass(Class<?> testClass) {
  12. return "哈哈哈";
  13. }
  14. }
  15. }

断言、假设

测试中核心之一,用于判断是否执行成功,在JUnit5中增加了些对lambdas的支持,例如:

  1. @Test
  2. void asserts() {
  3. assertEquals(1,2, () -> "1要是1");
  4. }

另外,还增加了假设

  1. @Test
  2. void assume() {
  3. assumingThat("DEV".equals(System.getenv("ENV")),
  4. () -> {
  5. // 如果不为true这里将不执行
  6. assertEquals(1, 1);
  7. });
  8. assumeTrue("DEV".equals(System.getenv("ENV")),
  9. () -> "Aborting test: not on developer workstation");
  10. // 如果不为true这里将不执行
  11. }

禁用

添加@Disabled()可以禁用测试,这个意义在于某一测试用例遇到问题,临时不执行,等待问题修复后再次使用的

  1. @Disabled("Disabled 因为重复")
  2. class TimeUtilsCopyTest {}

测试执行条件

通过添加 @EnabledOnOs 或者 @DisabledOnOs 来决定在某一操作系统上执行.

  1. @Test
  2. @EnabledOnOs(MAC)
  3. void testOnMac() {
  4. log.info("exec on mac");
  5. }
  6. @Test
  7. @EnabledOnOs({ WINDOWS, LINUX })
  8. void testOnOs() {
  9. log.info("exec on windows or linux");
  10. }

@EnabledOnJre@DisabledOnJre 可以对java环境判断

  1. @Test
  2. @EnabledOnJre(JRE.JAVA_8)
  3. void testOnJava8() {
  4. log.info("exec on java 8");
  5. }

@EnabledIfSystemProperty/@DisabledIfSystemProperty@EnabledIfEnvironmentVariable/@DisabledIfEnvironmentVariable 分别判断系统和环境变量,他们的匹配项支持正则表达式

  1. @Test
  2. @DisabledIfEnvironmentVariable(named = "ENV", matches = ".*development.*")
  3. void notOnDeveloperWorkstation() {
  4. // ...
  5. }

标签/分组

JUnit5 中支持通过 @Tag() 对测试用例进行分组,例如

  1. @Tag("conditional")
  2. @Test
  3. @EnabledOnOs(MAC)
  4. void testOnMac() {
  5. log.info("exec on mac");
  6. }
  7. @Tag("conditional")
  8. @Test
  9. @EnabledOnJre(JRE.JAVA_8)
  10. void testOnJava8() {
  11. log.info("exec on java 8");
  12. }

@Tag() 有以下这些语法规则

  • 不能为null或者空字符串
  • 不能有空格
  • 不能包含ISO控制符
  • 不能包含保留字符(,,(,),&,|,!)

顺序

添加@TestMethodOrder(MethodOrderer.OrderAnnotation.class)与@Order(),定义测试用例的执行顺序

  1. @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
  2. public class OrderedTest {
  3. @Test
  4. @Order(2)
  5. void emptyValues() {
  6. // perform assertions against empty values
  7. }
  8. @Test
  9. @Order(1)
  10. void nullValues() {
  11. // perform assertions against null values
  12. }
  13. @Test
  14. @Order(3)
  15. void validValues() {
  16. // perform assertions against valid values
  17. }
  18. }

生命周期

JUnit5 提供了4个生命周期注解 @BeforeAll @AfterAll @BeforeEach @AfterEach

例如:

  1. @Slf4j
  2. @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
  3. @TestInstance(TestInstance.Lifecycle.PER_CLASS)
  4. public class LifecycleTest {
  5. int num = 0;
  6. @BeforeAll
  7. static void initAll() {
  8. log.error("initAll");
  9. }
  10. @BeforeEach
  11. void init() {
  12. log.error("init");
  13. }
  14. @Test
  15. @Order(1)
  16. void doTest1() {
  17. log.error("num is " + num);
  18. num = 1;
  19. log.error("doTest1");
  20. }
  21. @Test
  22. @Order(2)
  23. void doTest2() {
  24. log.error("num is " + num);
  25. num = 2;
  26. log.error("doTest1");
  27. }
  28. }

除此外,还有@TestInstance()配置,见上面的例子,这个存在两个模式

  • PER_METHOD:每个测试用例执行前,都会创建一个实例(默认,与junit4一致)
  • PER_CLASS:每个类的测试用例执行前,创建统一的实例

上面的例子中,得到的log为:

  1. 13:58:03.477 [main] ERROR com.jiangtj.example.junit5.LifecycleTest - initAll
  2. 13:58:03.485 [main] ERROR com.jiangtj.example.junit5.LifecycleTest - init
  3. 13:58:03.487 [main] ERROR com.jiangtj.example.junit5.LifecycleTest - num is 0
  4. 13:58:03.487 [main] ERROR com.jiangtj.example.junit5.LifecycleTest - doTest1
  5. 13:58:03.494 [main] ERROR com.jiangtj.example.junit5.LifecycleTest - init
  6. 13:58:03.495 [main] ERROR com.jiangtj.example.junit5.LifecycleTest - num is 1
  7. 13:58:03.495 [main] ERROR com.jiangtj.example.junit5.LifecycleTest - doTest1

doTest1() 的执行,影响到num属性的值,而默认模式下则不会

嵌套

@Nested() 可以更好的表达测试用例间的关系,例如官方的例子

  1. @DisplayName("A stack")
  2. class TestingAStackDemo {
  3. Stack<Object> stack;
  4. @Test
  5. @DisplayName("is instantiated with new Stack()")
  6. void isInstantiatedWithNew() {
  7. new Stack<>();
  8. }
  9. @Nested
  10. @DisplayName("when new")
  11. class WhenNew {
  12. @BeforeEach
  13. void createNewStack() {
  14. stack = new Stack<>();
  15. }
  16. @Test
  17. @DisplayName("is empty")
  18. void isEmpty() {
  19. assertTrue(stack.isEmpty());
  20. }
  21. @Test
  22. @DisplayName("throws EmptyStackException when popped")
  23. void throwsExceptionWhenPopped() {
  24. assertThrows(EmptyStackException.class, stack::pop);
  25. }
  26. @Test
  27. @DisplayName("throws EmptyStackException when peeked")
  28. void throwsExceptionWhenPeeked() {
  29. assertThrows(EmptyStackException.class, stack::peek);
  30. }
  31. @Nested
  32. @DisplayName("after pushing an element")
  33. class AfterPushing {
  34. String anElement = "an element";
  35. @BeforeEach
  36. void pushAnElement() {
  37. stack.push(anElement);
  38. }
  39. @Test
  40. @DisplayName("it is no longer empty")
  41. void isNotEmpty() {
  42. assertFalse(stack.isEmpty());
  43. }
  44. @Test
  45. @DisplayName("returns the element when popped and is empty")
  46. void returnElementWhenPopped() {
  47. assertEquals(anElement, stack.pop());
  48. assertTrue(stack.isEmpty());
  49. }
  50. @Test
  51. @DisplayName("returns the element when peeked but remains not empty")
  52. void returnElementWhenPeeked() {
  53. assertEquals(anElement, stack.peek());
  54. assertFalse(stack.isEmpty());
  55. }
  56. }
  57. }
  58. }

我们可以清晰的看到他们之间的关系

image

重复测试

@RepeatedTest() 执行多次测试,支持name修改名称(具体见官网,觉得没多大意义),另外可以在方法中获取repetitionInfo参数,用于判断当前的执行情况(JUnit5支持注入参数,后续详说)

  1. @Slf4j
  2. class RepeatedTestsDemo {
  3. @RepeatedTest(2)
  4. void repeatedTest() {
  5. log.info("done!");
  6. }
  7. @RepeatedTest(2)
  8. void repeatedTest2(RepetitionInfo repetitionInfo) {
  9. int currentRepetition = repetitionInfo.getCurrentRepetition();
  10. int totalRepetitions = repetitionInfo.getTotalRepetitions();
  11. log.info(String.format("About to execute repetition %d of %d", //
  12. currentRepetition, totalRepetitions));
  13. }
  14. }

参数测试

@ParameterizedTest 很实用的注解,需要junit-jupiter-params依赖(我们已经添加了)

它主要是配置@xxxSource,注入参数,以完成测试,参数的注入方式有多种

数据源

@ValueSource 注入String内容,这是最常用的

  1. @ParameterizedTest
  2. @ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
  3. void palindromes(String candidate) {
  4. log.error(candidate);
  5. }

@EnumSource 注入枚举类

  1. @ParameterizedTest
  2. @EnumSource(TimeUnit.class)
  3. void testWithEnumSource(TimeUnit timeUnit) {
  4. log.error(timeUnit.toString());
  5. }
  6. @ParameterizedTest
  7. @EnumSource(value = TimeUnit.class, names = { "DAYS", "HOURS" })
  8. void testWithEnumSourceInclude(TimeUnit timeUnit) {
  9. // 选择部分
  10. log.error(timeUnit.toString());
  11. }

@MethodSource 通过方法名注入(我更倾向于使用下面的@ArgumentsSource

  1. @ParameterizedTest
  2. @MethodSource("stringProvider")
  3. void testWithExplicitLocalMethodSource(String argument) {
  4. log.error(argument);
  5. }
  6. static Stream<String> stringProvider() {
  7. return Stream.of("apple", "banana");
  8. }
  9. @ParameterizedTest
  10. @MethodSource("stringIntAndListProvider")
  11. void testWithMultiArgMethodSource(String str, int num, List<String> list) {
  12. // 多参支持
  13. log.error(String.format("Content: %s is %d, %s", str, num, String.join(",", list)));
  14. }
  15. static Stream<Arguments> stringIntAndListProvider() {
  16. return Stream.of(
  17. arguments("apple", 1, Arrays.asList("a", "b")),
  18. arguments("lemon", 2, Arrays.asList("x", "y"))
  19. );
  20. }

@CsvSource csv源支持

  1. @ParameterizedTest
  2. @CsvSource({
  3. "apple, 1",
  4. "banana, 2",
  5. "'lemon, lime', 0xF1"
  6. })
  7. void testWithCsvSource(String fruit, int rank) {
  8. log.error(fruit + rank);
  9. }

它也支持从文件导入,例如@CsvFileSource(resources = "/two-column.csv", numLinesToSkip = 1)

@ArgumentsSource 通过自定义的参数提供器导入

  1. @ParameterizedTest
  2. @ArgumentsSource(MyArgumentsProvider.class)
  3. void testWithArgumentsSource(String argument) {
  4. log.error(argument);
  5. }
  6. static class MyArgumentsProvider implements ArgumentsProvider {
  7. @Override
  8. public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
  9. return Stream.of("apple", "banana").map(Arguments::of);
  10. }
  11. }

参数转换

为了支持csv,JUnit支持了些内建的转换,详细见文档writing-tests-parameterized-tests-argument-conversion,如果转换失败,会寻找构造器或者静态构造方法(非私有)中,单String的方法,来转换对应的对象

内建的转换有必要,但后一种,我宁愿得到报错,而不是转换,隐形的转换往往会导致莫名的问题出现

所以推荐通过@ConvertWith实现参数类型间的转换

  1. @ParameterizedTest
  2. @ValueSource(strings = { "Wow,12", "radar,50"})
  3. void toBook(@ConvertWith(ToBookConverter.class) Book book) {
  4. log.error(book.toString());
  5. }
  6. static class ToBookConverter extends SimpleArgumentConverter {
  7. @Override
  8. protected Object convert(Object source, Class<?> targetType) {
  9. String value = String.valueOf(source);
  10. String[] split = value.split(",");
  11. return Book.of(split[0], Integer.parseInt(split[1]));
  12. }
  13. }

JUnit中也内置了些转换,如@JavaTimeConversionPattern

除外,还可以通过@AggregateWith转换或者接收ArgumentsAccessor对象

Dynamic测试

除了常规的@Test,我们还可以通过@TestFactory来构建整个测试树

  1. class DynamicTestsDemo {
  2. private final Calculator calculator = new Calculator();
  3. // This will result in a JUnitException!
  4. @TestFactory
  5. List<String> dynamicTestsWithInvalidReturnType() {
  6. return Arrays.asList("Hello");
  7. }
  8. @TestFactory
  9. Collection<DynamicTest> dynamicTestsFromCollection() {
  10. return Arrays.asList(
  11. dynamicTest("1st dynamic test", () -> assertTrue(isPalindrome("madam"))),
  12. dynamicTest("2nd dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
  13. );
  14. }
  15. @TestFactory
  16. Iterable<DynamicTest> dynamicTestsFromIterable() {
  17. return Arrays.asList(
  18. dynamicTest("3rd dynamic test", () -> assertTrue(isPalindrome("madam"))),
  19. dynamicTest("4th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
  20. );
  21. }
  22. @TestFactory
  23. Iterator<DynamicTest> dynamicTestsFromIterator() {
  24. return Arrays.asList(
  25. dynamicTest("5th dynamic test", () -> assertTrue(isPalindrome("madam"))),
  26. dynamicTest("6th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
  27. ).iterator();
  28. }
  29. @TestFactory
  30. DynamicTest[] dynamicTestsFromArray() {
  31. return new DynamicTest[] {
  32. dynamicTest("7th dynamic test", () -> assertTrue(isPalindrome("madam"))),
  33. dynamicTest("8th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
  34. };
  35. }
  36. @TestFactory
  37. Stream<DynamicTest> dynamicTestsFromStream() {
  38. return Stream.of("racecar", "radar", "mom", "dad")
  39. .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))));
  40. }
  41. @TestFactory
  42. Stream<DynamicTest> dynamicTestsFromIntStream() {
  43. // Generates tests for the first 10 even integers.
  44. return IntStream.iterate(0, n -> n + 2).limit(10)
  45. .mapToObj(n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0)));
  46. }
  47. @TestFactory
  48. Stream<DynamicTest> generateRandomNumberOfTests() {
  49. // Generates random positive integers between 0 and 100 until
  50. // a number evenly divisible by 7 is encountered.
  51. Iterator<Integer> inputGenerator = new Iterator<Integer>() {
  52. Random random = new Random();
  53. int current;
  54. @Override
  55. public boolean hasNext() {
  56. current = random.nextInt(100);
  57. return current % 7 != 0;
  58. }
  59. @Override
  60. public Integer next() {
  61. return current;
  62. }
  63. };
  64. // Generates display names like: input:5, input:37, input:85, etc.
  65. Function<Integer, String> displayNameGenerator = (input) -> "input:" + input;
  66. // Executes tests based on the current input value.
  67. ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7 != 0);
  68. // Returns a stream of dynamic tests.
  69. return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);
  70. }
  71. @TestFactory
  72. Stream<DynamicNode> dynamicTestsWithContainers() {
  73. return Stream.of("A", "B", "C")
  74. .map(input -> dynamicContainer("Container " + input, Stream.of(
  75. dynamicTest("not null", () -> assertNotNull(input)),
  76. dynamicContainer("properties", Stream.of(
  77. dynamicTest("length > 0", () -> assertTrue(input.length() > 0)),
  78. dynamicTest("not empty", () -> assertFalse(input.isEmpty()))
  79. ))
  80. )));
  81. }
  82. @TestFactory
  83. DynamicNode dynamicNodeSingleTest() {
  84. return dynamicTest("'pop' is a palindrome", () -> assertTrue(isPalindrome("pop")));
  85. }
  86. @TestFactory
  87. DynamicNode dynamicNodeSingleContainer() {
  88. return dynamicContainer("palindromes",
  89. Stream.of("racecar", "radar", "mom", "dad")
  90. .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text)))
  91. ));
  92. }
  93. }

还未看过源码,但目测@Test是由内建的转换器,转换成DynamicNode,然后再执行。使用@TestFactory,tree型的代码也是种选择,再维护上,不差于@Test的常规方案

后续还有扩展与Spring应用篇,欢迎关注我哦~

一个小疑问,JUnit5 的注解风格和 Spring 为何如此接近。。。

本文作者: Mr.J
本文链接: https://www.dnocm.com/articles/cherry/junit-5-info/
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

评论
已有0条评论
0/150
提交
热门评论