将参数注入到 JUnit Jupiter 单元测试
最后更新:2024年1月16日
1. 概述
在 JUnit 5 之前,为了引入一个很酷的新特性,JUnit 团队必须将其添加到核心 API 中。有了 JUnit 5,团队决定是时候将扩展核心 JUnit API 的能力推到 JUnit 本身之外了,这是 JUnit 5 的核心理念之一,被称为“优先扩展点而非特性”。
在本文中,我们将重点关注其中一个扩展点接口 – ParameterResolver – 你可以使用它将参数注入到你的测试方法中。 有几种不同的方法可以让 JUnit 平台意识到你的扩展(这个过程称为“注册”),并且在本文中,我们将重点关注声明式注册(即通过源代码进行注册)。
2. ParameterResolver
使用 JUnit 4 API 可以将参数注入到你的测试方法中,但它受到相当大的限制。 使用 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() – 如果参数的类型受支持(在本例中为 Foo),则返回 true,并且
- resolveParamater() – 提供正确类型的对象(在本例中为新的 Foo 实例),然后将其注入到你的测试方法中
2.2. FooTest
@ExtendWith(FooParameterResolver.class)
public class FooTest {
@Test
public void testIt(Foo fooInstance) {
// TEST CODE GOES HERE
}
}
然后要使用扩展,我们需要声明它 – 即告诉 JUnit 平台关于它 – 通过@ExtendWith 注解(第 1 行)。
当 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对象。 这是有效Person对象的存储库,每次 JUnit 平台调用resolveParameter()方法时,将从中随机选择一个。
将有效的 Person 对象放在这里可以实现两件事
- 将单元测试与其驱动数据之间的关注点分离
- 可重用性,如果其他单元测试需要有效的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 时才由 JUnit 平台调用。
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 数组中的Person对象。就像ValidPersonParameterResolver 一样,这个类包含一个“坏”(即无效)数据的存储,供单元测试使用,以确保,例如,在存在无效数据时,PersonValidator.ValidationExceptions 能够被正确抛出。
@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,是时候将它们投入使用了。让我们创建一个名为PersonValidatorTest 的 PersonValidator JUnit 测试类。
我们将使用仅在 JUnit Jupiter 中可用的几个特性
- @DisplayName – 这是在测试报告中显示的名称,更具可读性
- @Nested – 创建一个嵌套测试类,拥有自己的测试生命周期,与父类分离
- @RepeatedTest – 测试重复执行的次数由 value 属性指定(每个示例中为 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));
}
}
}
请注意,我们能够在同一个主测试类中使用ValidPersonParameterResolver 和 InvalidPersonParameterResolver 扩展 – 仅在 @Nested 类级别声明它们。尝试一下 JUnit 4! (剧透:你做不到!)
5. 结论
在本文中,我们探讨了如何编写两个ParameterResolver扩展 – 用来提供有效和无效的对象。然后我们了解了如何在单元测试中使用这两个ParameterResolver实现。
支持本文的代码可在 GitHub 上获取。 一旦你以 Baeldung Pro 会员 身份登录,就开始学习并在项目上进行编码。
如果您想了解有关 JUnit Jupiter 扩展模型的更多信息,请查看JUnit 5 用户指南,或developerWorks 上的我的教程的第二部分。















