4. 参数化测试:@ParameterizedTest、@ValueSource、@CsvSource、@MethodSource、参数组合

4.1 原理与价值

参数化测试(Parameterized Test)允许同一测试逻辑以不同输入数据重复执行,消除冗余代码并扩展边界覆盖。JUnit 5 通过 @ParameterizedTest 标记测试方法,配合多种数据源注解注入参数。Android 单元测试中,参数化特别适合验证业务逻辑、状态转换、边界值等场景。

  • 减少重复:不再为每组数据编写独立 @Test 方法。
  • 提升覆盖率:轻松添加大量输入组合,包括边界、异常值。
  • 可读性:数据与逻辑分离,测试意图更清晰。

4.2 实现流程

  1. 添加依赖:确保模块级 build.gradle 包含 junit-jupiter-params。
  2. 编写测试方法:使用 @ParameterizedTest 替代 @Test,方法签名包含参数。
  3. 选择数据源:根据数据量、格式、复杂度选用 @ValueSource、@CsvSource、@MethodSource 或 @CsvFileSource。
  4. 执行与断言:每个数据组会生成独立测试用例,可单独查看通过/失败状态。
提示:Android 中使用 @RunWith(AndroidJUnit4.class) 时,参数化测试仍适用,但需将 @ParameterizedTest 与 @Test 分开放置在同一个类中。

4.3 核心注解与代码示例

4.3.1 @ValueSource – 简单基本类型数据

@ParameterizedTest
@ValueSource(strings = {"apple", "banana", "cherry"})
void testFruitLength(String fruit) {
    assertTrue(fruit.length() > 0);
}

支持 ints, longs, doubles, booleans 等基本类型数组。

4.3.2 @CsvSource – 多参数/CSV格式

@ParameterizedTest
@CsvSource({
    "1,   1,   2",
    "2,   3,   5",
    "10, -5,   5"
})
void testAdd(int a, int b, int expected) {
    assertEquals(expected, Calculator.add(a, b));
}

每行为一组参数,默认逗号分隔,可使用 delimiter 或 delimiterString 自定义。

4.3.3 @MethodSource – 复杂或动态数据

@ParameterizedTest
@MethodSource("provideUserData")
void testUserValidation(User user, boolean expected) {
    assertEquals(expected, validator.isValid(user));
}

static Stream<Arguments> provideUserData() {
    return Stream.of(
        Arguments.of(new User("Alice", 20), true),
        Arguments.of(new User("", 17), false)
    );
}

@MethodSource 引用本类或外部类的静态工厂方法,返回 Stream<Arguments>、Iterable 或数组。方法名默认为测试方法名+"Provider",需显式指定。

4.3.4 @CsvFileSource – 外部CSV文件

@ParameterizedTest
@CsvFileSource(resources = "/test-data/orders.csv", numLinesToSkip = 1)
void testOrderTotal(double price, int quantity, double expected) {
    assertEquals(expected, price * quantity, 0.01);
}

CSV 文件需放在 src/test/resources 下,numLinesToSkip 可跳过表头。

4.3.5 参数组合与多参数注入

可使用 @CsvSource 或 @MethodSource 注入多个参数,JUnit 5 会将数据按顺序映射到方法参数。组合多个注解也能实现笛卡尔积式测试:

@ParameterizedTest
@ValueSource(strings = {"A", "B"})
@ValueSource(ints = {1, 2})
void testCombination(String s, int i) {
    // 每个 ValueSource 的数据会组合运行?❌ 实际 JUnit 不支持多 @ValueSource 叠加
    // 正确做法:使用 @MethodSource 返回组合参数
}

官方推荐使用 @MethodSource 手动组合:

static Stream<Arguments> combineParams() {
    return Stream.of(
        Arguments.of("A", 1),
        Arguments.of("A", 2),
        Arguments.of("B", 1),
        Arguments.of("B", 2)
    );
}

4.4 数据源注解对比

注解 数据来源 参数数量 适用场景
@ValueSource 内联数组 1个参数 简单基本类型或字符串常量
@CsvSource 内联CSV行 多个参数 少量固定组合数据
@CsvFileSource 外部CSV文件 多个参数 大量数据或可维护性要求高
@MethodSource 静态工厂方法 任意参数 动态生成、复杂对象、组合参数
@ArgumentsSource 自定义 ArgumentsProvider 任意参数 高级自定义逻辑
常见错误:使用 @ValueSource 传递 null 时,需使用 @NullSource 或 @EmptySource 注解补充;基本类型不支持 null。参数数量与方法签名不匹配会导致 ParameterResolutionException。静态 @MethodSource 方法必须返回 Stream、Iterable 或数组,且不能为 private。

4.5 调试与常见错误

  • 数据源为空:测试用例数为0,应检查数据提供方法是否返回有效元素或文件路径是否正确。
  • 类型转换失败:CSV或ValueSource中字符串与目标类型不匹配,需手动转换(如 @ConvertWith)。
  • Android环境特殊:在主线程中使用参数化测试时,注意 Looper 相关限制;可配合 @UiThreadTest 或移除Android依赖进行纯JUnit测试。
  • IDE报告:IntelliJ/Android Studio 会为每个数据组生成单独测试节点,方便定位失败行。

4.6 性能与稳定性优化

  • 延迟加载:@MethodSource 返回 Stream 时确保是 lazy,避免一次性生成全部数据导致OOM。
  • 数据复用:对消耗较大的对象,可考虑在 @MethodSource 中缓存或使用 @TestInstance(TestInstance.Lifecycle.PER_CLASS)。
  • 参数化与并发:JUnit 5 默认顺序执行每个数据组,如需并发可结合 @Execution(ExecutionMode.CONCURRENT) 但需注意 Android 测试环境的稳定性。
  • 减少CSV文件I/O:对于大规模数据,优先使用 @CsvFileSource 而非内联CSV,但应控制文件行数(建议 <5000 行/测试类)。

4.7 参数化测试工作流程

参数化测试数据流 @ParameterizedTest @ValueSource / @CsvSource @MethodSource / @CsvFileSource 静态方法 / 外部文件 参数注入 → 断言执行 每个数据组生成独立测试用例,IDE 可单独查看结果。
最佳实践:在 Android 项目中,将参数化测试放到独立测试类(如 LoginValidatorParamTest),与普通 @Test 方法分离,便于维护和并发执行。结合 @DisplayName("...") 和 @ParameterizedTest(name = "[{index}] {arguments}") 为每个用例提供可读名称。