1. 概述
在JUnit 5之前,要引入新功能,JUnit团队必须修改核心API。而JUnit 5团队决定将扩展核心API的能力移出JUnit本身,这体现了JUnit 5的核心哲学——“优先扩展点而非功能”。
本文将聚焦其中一个扩展点接口——ParameterResolver,你可以用它向测试方法注入参数。注册扩展(让JUnit平台感知扩展的过程)有几种方式,本文重点介绍声明式注册(即通过源代码注册)。
2. ParameterResolver
JUnit 4也能向测试方法注入参数,但功能相当有限。JUnit 5的Jupiter API可通过实现ParameterResolver进行扩展,为测试方法提供任意类型的对象。我们来看看具体实现。
2.1. FooParameterResolver
public class FooParameterResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
return parameterContext.getParameter().getType() == Foo.class;
}
@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
return new Foo();
}
}
首先需要实现ParameterResolver接口,它包含两个方法:
- supportsParameter() - 当参数类型受支持时返回true(本例中为Foo类型)
- resolveParameter() - 提供正确类型的对象(本例中返回新的Foo实例),该对象将被注入测试方法
2.2. FooTest
@ExtendWith(FooParameterResolver.class)
public class FooTest {
@Test
public void testIt(Foo fooInstance) {
// TEST CODE GOES HERE
}
}
使用扩展时需通过*@ExtendWith*注解声明(第1行),让JUnit平台感知它的存在。
当JUnit平台运行单元测试时,会从FooParameterResolver获取Foo实例并传递给*testIt()*方法(第4行)。
扩展有作用域,根据声明位置激活扩展:
- 方法级:仅对当前方法生效
- 类级:对整个测试类或*@Nested*测试类生效(后续会看到)
⚠️ 注意:不要在同一参数类型的两个作用域同时声明ParameterResolver,否则JUnit平台会因歧义报错。
本文将编写两个扩展来注入Person对象:一个注入“有效”数据(ValidPersonParameterResolver),一个注入“无效”数据(InvalidPersonParameterResolver)。用这些数据测试PersonValidator类,验证Person对象的状态。
3. 编写扩展
理解了ParameterResolver扩展后,现在编写:
- 提供有效Person对象的扩展(ValidPersonParameterResolver)
- 提供无效Person对象的扩展(InvalidPersonParameterResolver)
3.1. ValidPersonParameterResolver
public class ValidPersonParameterResolver implements ParameterResolver {
public static Person[] VALID_PERSONS = {
new Person().setId(1L).setLastName("Adams").setFirstName("Jill"),
new Person().setId(2L).setLastName("Baker").setFirstName("James"),
new Person().setId(3L).setLastName("Carter").setFirstName("Samanta"),
new Person().setId(4L).setLastName("Daniels").setFirstName("Joseph"),
new Person().setId(5L).setLastName("English").setFirstName("Jane"),
new Person().setId(6L).setLastName("Fontana").setFirstName("Enrique"),
};
注意VALID_PERSONS数组——这是有效Person对象的仓库。每次JUnit平台调用*resolveParameter()*时,会从中随机选择一个对象。
这样做有两个好处:
- 分离单元测试与驱动数据
- 复用性:其他需要有效Person对象的单元测试可直接使用
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
boolean ret = false;
if (parameterContext.getParameter().getType() == Person.class) {
ret = true;
}
return ret;
}
当参数类型为Person时,扩展告知JUnit平台支持该类型,否则返回false。
为什么需要这个判断?虽然本文示例简单,但实际应用中单元测试类可能庞大复杂,包含多种参数类型的测试方法。JUnit平台解析参数时,必须检查当前作用域内所有注册的ParameterResolver。
@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
Object ret = null;
if (parameterContext.getParameter().getType() == Person.class) {
ret = VALID_PERSONS[new Random().nextInt(VALID_PERSONS.length)];
}
return ret;
}
从VALID_PERSONS数组返回随机Person对象。注意:resolveParameter()仅在supportsParameter()返回true时被调用。
3.2. InvalidPersonParameterResolver
public class InvalidPersonParameterResolver implements ParameterResolver {
public static Person[] INVALID_PERSONS = {
new Person().setId(1L).setLastName("Ad_ams").setFirstName("Jill,"),
new Person().setId(2L).setLastName(",Baker").setFirstName(""),
new Person().setId(3L).setLastName(null).setFirstName(null),
new Person().setId(4L).setLastName("Daniel&").setFirstName("{Joseph}"),
new Person().setId(5L).setLastName("").setFirstName("English, Jane"),
new Person()/*.setId(6L).setLastName("Fontana").setFirstName("Enrique")*/,
};
注意INVALID_PERSONS数组——与ValidPersonParameterResolver类似,这里存储“坏数据”(无效数据)。单元测试可用这些数据验证PersonValidator在遇到无效数据时是否正确抛出ValidationException:
@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
Object ret = null;
if (parameterContext.getParameter().getType() == Person.class) {
ret = INVALID_PERSONS[new Random().nextInt(INVALID_PERSONS.length)];
}
return ret;
}
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
boolean ret = false;
if (parameterContext.getParameter().getType() == Person.class) {
ret = true;
}
return ret;
}
其余部分与“有效数据”扩展行为完全一致。
4. 声明并使用扩展
现在有两个ParameterResolver,是时候派上用场了。为PersonValidator创建JUnit测试类PersonValidatorTest。
我们将使用JUnit Jupiter的几个特性:
- @DisplayName - 测试报告显示的名称,更易读
- @Nested - 创建嵌套测试类,拥有独立的生命周期
- @RepeatedTest - 按指定次数重复测试(示例中为10次)
通过*@Nested*类,可在同一测试类中同时测试有效和无效数据,且彼此完全隔离:
@DisplayName("Testing PersonValidator")
public class PersonValidatorTest {
@Nested
@DisplayName("When using Valid data")
@ExtendWith(ValidPersonParameterResolver.class)
public class ValidData {
@RepeatedTest(value = 10)
@DisplayName("All first names are valid")
public void validateFirstName(Person person) {
try {
assertTrue(PersonValidator.validateFirstName(person));
} catch (PersonValidator.ValidationException e) {
fail("Exception not expected: " + e.getLocalizedMessage());
}
}
}
@Nested
@DisplayName("When using Invalid data")
@ExtendWith(InvalidPersonParameterResolver.class)
public class InvalidData {
@RepeatedTest(value = 10)
@DisplayName("All first names are invalid")
public void validateFirstName(Person person) {
assertThrows(
PersonValidator.ValidationException.class,
() -> PersonValidator.validateFirstName(person));
}
}
}
✅ 关键点:通过仅在*@Nested类级别声明扩展,我们在同一主测试类中同时使用了ValidPersonParameterResolver和InvalidPersonParameterResolver*。试试用JUnit 4实现这个功能?(剧透:做不到!)
5. 总结
本文探讨了如何编写两个ParameterResolver扩展——分别提供有效和无效对象。然后展示了如何在单元测试中使用这两个实现。
完整代码请访问GitHub仓库。
想深入了解JUnit Jupiter扩展模型?可参考: