bigywater.tech
Java · 324

JUnit 参数化测试

参数化测试每组参数到底算几次测试?CSV 里的引号和 null 怎么写?类型转换失败、参数个数不对、来源是空的会发生什么?每次调用有没有新实例和 @BeforeEach?这篇逐个运行,看树形输出里的真实显示名。

4 → 3 + 1
@CsvSource 4 行数据的结果:3 次通过、1 次失败(失败不影响其它)
新实例
每次调用都构造新的测试类实例并运行 @BeforeEach / @AfterEach
0 tests
@ValueSource(ints = {}) 为空时找到的测试数,并抛 PreconditionViolationException

来源与显示名

实测 · 本机执行输出
  @ValueSource(ints = {1, 2, 3})
        [BeforeEach] 实例 #1
        [AfterEach] 实例 #1
        [BeforeEach] 实例 #2
        [AfterEach] 实例 #2
        [BeforeEach] 实例 #3
        [AfterEach] 实例 #3
    │  └─ PT ✔
    │     └─ valueSource(int) ✔
    │        ├─ [1] 1 ✔
    │        ├─ [2] 2 ✔
    │        └─ [3] 3 ✔
    汇总:3 tests found ] | 3 tests successful ] | 0 tests failed ]
  @CsvSource + name = "[{index}] {0} + {1} = {2}",第三行故意写错
        [BeforeEach] 实例 #1
        [AfterEach] 实例 #1
        [BeforeEach] 实例 #2
        [AfterEach] 实例 #2
        [BeforeEach] 实例 #3
        [AfterEach] 实例 #3
        [BeforeEach] 实例 #4
        [AfterEach] 实例 #4
    │  └─ PT ✔
    │     └─ csv(int, int, int) ✔
    │        ├─ [1] 1 + 2 = 3 ✔
    │        ├─ [2] 4 + 5 = 9 ✔
    │        ├─ [3] 10 + 20 = 31 ✘ expected: <31> but was: <30>
    │        └─ [4] 7 + 0 = 7 ✔
    Failures (1):
        MethodSource [className = 'PT', methodName = 'csv', methodParameterTypes = 'int, int, int']
        => org.opentest4j.AssertionFailedError: expected: <31> but was: <30>
    汇总:4 tests found ] | 3 tests successful ] | 1 tests failed ]
  @CsvSource 的引号、nullValues 与空串
        [BeforeEach] 实例 #1
        收到 s=[apple] n=1
        [AfterEach] 实例 #1
        [BeforeEach] 实例 #2
        收到 s=[banana, ripe] n=2
        [AfterEach] 实例 #2
        [BeforeEach] 实例 #3
        收到 s=null n=3
        [AfterEach] 实例 #3
        [BeforeEach] 实例 #4
        收到 s=[] n=4
        [AfterEach] 实例 #4
        [BeforeEach] 实例 #5
        收到 s=null n=5
        [AfterEach] 实例 #5
    │  └─ PT ✔
    │     └─ csvQuoting(String, int) ✔
    │        ├─ [1] apple, 1 ✔
    │        ├─ [2] banana, ripe, 2 ✔
    │        ├─ [3] null, 3 ✔
    │        ├─ [4] , 4 ✔
    │        └─ [5] null, 5 ✔
    汇总:5 tests found ] | 5 tests successful ] | 0 tests failed ]
  @EnumSource(names = SECONDS, MINUTES)
        [BeforeEach] 实例 #1
        [AfterEach] 实例 #1
        [BeforeEach] 实例 #2
        [AfterEach] 实例 #2
    │  └─ PT ✔
    │     └─ enumSource(TimeUnit) ✔
    │        ├─ [1] SECONDS ✔
    │        └─ [2] MINUTES ✔
    汇总:2 tests found ] | 2 tests successful ] | 0 tests failed ]
  @EnumSource(mode = EXCLUDE, 排除前五个)
        [BeforeEach] 实例 #1
        [AfterEach] 实例 #1
        [BeforeEach] 实例 #2
        [AfterEach] 实例 #2
    │  └─ PT ✔
    │     └─ enumExclude(TimeUnit) ✔
    │        ├─ [1] HOURS ✔
    │        └─ [2] DAYS ✔
    汇总:2 tests found ] | 2 tests successful ] | 0 tests failed ]
  @MethodSource
        [MethodSource 工厂方法被调用]
        [BeforeEach] 实例 #1
        [AfterEach] 实例 #1
        [BeforeEach] 实例 #2
        [AfterEach] 实例 #2
    │  └─ PT ✔
    │     └─ methodSource(String, int) ✔
    │        ├─ [1] a, 1 ✔
    │        └─ [2] bb, 2 ✔
    汇总:2 tests found ] | 2 tests successful ] | 0 tests failed ]
  @NullSource + @EmptySource + @ValueSource
        [BeforeEach] 实例 #1
        收到 null
        [AfterEach] 实例 #1
        [BeforeEach] 实例 #2
        收到 []
        [AfterEach] 实例 #2
        [BeforeEach] 实例 #3
        收到 [ ]
        [AfterEach] 实例 #3
        [BeforeEach] 实例 #4
        收到 [x]
        [AfterEach] 实例 #4
    │  └─ PT ✔
    │     └─ nullEmpty(String) ✔
    │        ├─ [1] null ✔
    │        ├─ [2]  ✔
    │        ├─ [3]   ✔
    │        └─ [4] x ✔
    汇总:4 tests found ] | 4 tests successful ] | 0 tests failed ]

参数转换与错误

实测 · 本机执行输出
  隐式转换 String → LocalDate
        [BeforeEach] 实例 #1
        隐式转换 String → LocalDate:2024-02-29
        [AfterEach] 实例 #1
        [BeforeEach] 实例 #2
        隐式转换 String → LocalDate:2023-12-31
        [AfterEach] 实例 #2
    │  └─ PT ✔
    │     └─ implicitConversion(LocalDate) ✔
    │        ├─ [1] 2024-02-29 ✔
    │        └─ [2] 2023-12-31 ✔
    汇总:2 tests found ] | 2 tests successful ] | 0 tests failed ]
  转换失败:"abc" → double
        [BeforeEach] 实例 #1
        [AfterEach] 实例 #1
        [BeforeEach] 实例 #2
        [AfterEach] 实例 #2
    │  └─ PT ✔
    │     └─ badConversion(double) ✔
    │        ├─ [1] 3.14 ✔
    │        └─ [2] abc ✘ Error converting parameter at index 0: Failed to convert String "abc" to type double
    Failures (1):
        MethodSource [className = 'PT', methodName = 'badConversion', methodParameterTypes = 'double']
        => org.junit.jupiter.api.extension.ParameterResolutionException: Error converting parameter at index 0: Failed to convert String "abc" to type double
         Caused by: org.junit.jupiter.params.converter.ArgumentConversionException: Failed to convert String "abc" to type double
         Caused by: org.junit.platform.commons.support.conversion.ConversionException: Failed to convert String "abc" to type double
         Caused by: java.lang.NumberFormatException: For input string: "abc"
    汇总:2 tests found ] | 1 tests successful ] | 1 tests failed ]
  ArgumentsAccessor 聚合
        [BeforeEach] 实例 #1
        ArgumentsAccessor:alice,30,true(共 3 个)
        [AfterEach] 实例 #1
        [BeforeEach] 实例 #2
        ArgumentsAccessor:bob,25,false(共 3 个)
        [AfterEach] 实例 #2
    │  └─ PT ✔
    │     └─ aggregation(ArgumentsAccessor) ✔
    │        ├─ [1] alice, 30, true ✔
    │        └─ [2] bob, 25, false ✔
    汇总:2 tests found ] | 2 tests successful ] | 0 tests failed ]
  显式 @ConvertWith
        [BeforeEach] 实例 #1
        显式转换器得到 Person[name=alice, age=0]
        [AfterEach] 实例 #1
        [BeforeEach] 实例 #2
        显式转换器得到 Person[name=bob, age=0]
        [AfterEach] 实例 #2
    │  └─ PT ✔
    │     └─ explicitConverter(Person) ✔
    │        ├─ [1] alice ✔
    │        └─ [2] bob ✔
    汇总:2 tests found ] | 2 tests successful ] | 0 tests failed ]
  方法参数比来源多(两个参数、来源只有一个值)
        [BeforeEach] 实例 #1
        [AfterEach] 实例 #1
        [BeforeEach] 实例 #2
        [AfterEach] 实例 #2
    │  └─ PT ✔
    │     └─ tooMany(int, int) ✔
    │        ├─ [1] 1 ✘ No ParameterResolver registered for parameter [int arg1] in method [void PT.tooMany(int,int)].
    │        └─ [2] 2 ✘ No ParameterResolver registered for parameter [int arg1] in method [void PT.tooMany(int,int)].
    Failures (2):
        MethodSource [className = 'PT', methodName = 'tooMany', methodParameterTypes = 'int, int']
        => org.junit.jupiter.api.extension.ParameterResolutionException: No ParameterResolver registered for parameter [int arg1] in method [void PT.tooMany(int,int)].
        MethodSource [className = 'PT', methodName = 'tooMany', methodParameterTypes = 'int, int']
        => org.junit.jupiter.api.extension.ParameterResolutionException: No ParameterResolver registered for parameter [int arg1] in method [void PT.tooMany(int,int)].
    汇总:2 tests found ] | 0 tests successful ] | 2 tests failed ]
  来源为空
    │  └─ PT ✔
    │     └─ emptySource(int) ✘ Exactly one type of input must be provided in the @ValueSource annotation, but there were 0
    Failures (1):
        MethodSource [className = 'PT', methodName = 'emptySource', methodParameterTypes = 'int']
        => org.junit.platform.commons.PreconditionViolationException: Exactly one type of input must be provided in the @ValueSource annotation, but there were 0
    汇总:0 tests found ] | 0 tests successful ] | 0 tests failed ]

生命周期

实测 · 本机执行输出
  一次调用失败(n == 2)
        [BeforeEach] 实例 #1
        n=1 通过
        [AfterEach] 实例 #1
        [BeforeEach] 实例 #2
        [AfterEach] 实例 #2
        [BeforeEach] 实例 #3
        n=3 通过
        [AfterEach] 实例 #3
    │  └─ PT ✔
    │     └─ oneFails(int) ✔
    │        ├─ [1] 1 ✔
    │        ├─ [2] 2 ✘ n 不能是 2 ==> expected: <true> but was: <false>
    │        └─ [3] 3 ✔
    Failures (1):
        MethodSource [className = 'PT', methodName = 'oneFails', methodParameterTypes = 'int']
        => org.opentest4j.AssertionFailedError: n 不能是 2 ==> expected: <true> but was: <false>
    汇总:3 tests found ] | 2 tests successful ] | 1 tests failed ]
  每次调用都有新的测试类实例 #id 与 BeforeEach / AfterEach(@ValueSource(ints={1,2}) + TestInfo)
        [BeforeEach] 实例 #1
        TestInfo 参数:[1] 1
        [AfterEach] 实例 #1
        [BeforeEach] 实例 #2
        TestInfo 参数:[2] 2
        [AfterEach] 实例 #2
    │  └─ PT ✔
    │     └─ withInfo(int, TestInfo) ✔
    │        ├─ [1] 1 ✔
    │        └─ [2] 2 ✔
    汇总:2 tests found ] | 2 tests successful ] | 0 tests failed ]
  @RepeatedTest(3)
        [BeforeEach] 实例 #1
        重复 1/3
        [AfterEach] 实例 #1
        [BeforeEach] 实例 #2
        重复 2/3
        [AfterEach] 实例 #2
        [BeforeEach] 实例 #3
        重复 3/3
        [AfterEach] 实例 #3
    │  └─ PT ✔
    │     └─ repeated(RepetitionInfo) ✔
    │        ├─ repetition 1 of 3 ✔
    │        ├─ repetition 2 of 3 ✔
    │        └─ repetition 3 of 3 ✔
    汇总:3 tests found ] | 3 tests successful ] | 0 tests failed ]

分步演示

JUnit 参数化测试
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12;junit-platform-console-standalone 1.11.3(JUnit Jupiter 5.11.3,含 junit-jupiter-params);--details=tree
    • 没有验证:@CsvFileSource 与资源文件、@ArgumentsSource 自定义提供者、@ParameterizedTest 的 argumentCountValidation(5.12+)、并行执行、@ParameterizedTest 与 @TestTemplate 的底层关系、其它转换器(如 @JavaTimeConversionPattern)
    京ICP备2026067502号-1京公网安备11010502063487号