1. 概述
JUnit 5 引入了一些强大的功能,包括对参数化测试的支持。编写参数化测试可以节省大量时间,并且在很多情况下,只需要简单的注解组合就能启用。
然而,不正确的配置可能导致难以调试的异常,因为JUnit会在幕后管理许多测试执行的细节。
其中一个异常就是 ParameterResolutionException:
org.junit.jupiter.api.extension.ParameterResolutionException: No ParameterResolver registered for parameter ...
在这篇教程中,我们将探讨这个异常的原因以及如何解决它。
2. JUnit 5 的 ParameterResolver
要理解这个异常的原因,我们首先需要明白消息中所缺失的是什么:一个 ParameterResolver。
在JUnit 5中,引入了ParameterResolver接口,允许开发者扩展JUnit的基本功能,编写接受任何类型参数的测试。让我们看看一个简单的ParameterResolver实现:
public class FooParameterResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
// Parameter support logic
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
// Parameter resolution logic
}
}
我们可以看到类有两个主要方法:
- supportsParameter(): 判断参数类型是否支持
- resolveParameter(): 返回用于测试执行的参数
由于在没有ParameterResolver实现时会抛出ParameterResolutionException,我们暂时不会过多关注实现细节。现在,让我们先讨论可能导致异常的一些潜在原因。
3. ParameterResolutionException
ParameterResolutionException可能很难调试,特别是对于不太熟悉参数化测试的人来说。
首先,让我们定义一个简单的Book类,我们将为其编写单元测试:
public class Book {
private String title;
private String author;
// Standard getters and setters
}
为了示例,我们将写一些验证不同标题值的Book单元测试。我们先从两个非常简单的测试开始:
@Test
void givenWutheringHeights_whenCheckingTitleLength_thenTitleIsPopulated() {
Book wuthering = new Book("Wuthering Heights", "Charlotte Bronte");
assertThat(wuthering.getTitle().length()).isGreaterThan(0);
}
@Test
void givenJaneEyre_whenCheckingTitleLength_thenTitleIsPopulated() {
Book jane = new Book("Jane Eyre", "Charlotte Bronte");
assertThat(wuthering.getTitle().length()).isGreaterThan(0);
}
很容易看出这两个测试基本上是在做同样的事情:设置Book的标题并检查长度。我们可以简化测试,将它们合并成一个参数化测试。让我们讨论一下这种重构可能出错的方式。
3.1. 将参数传递给 @Test 方法
如果我们采取非常直接的方法,可能会认为将参数传递给带有*@Test*注解的方法就足够了:
@Test
void givenTitleAndAuthor_whenCreatingBook_thenFieldsArePopulated(String title, String author) {
Book book = new Book(title, author);
assertThat(book.getTitle().length()).isGreaterThan(0);
assertThat(book.getAuthor().length()).isGreaterThan(0);
}
代码编译并通过,但仔细想想,我们应该质疑这些参数是从哪里来的。运行这个例子,我们会看到一个异常:
org.junit.jupiter.api.extension.ParameterResolutionException: No ParameterResolver registered for parameter [java.lang.String arg0] in method ...
JUnit无法知道应将哪些参数传递给测试方法。
让我们继续重构我们的单元测试,看看ParameterResolutionException的另一个原因。
3.2. 竞争注解
我们可以在之前提到的方式下,通过一个ParameterResolver提供缺失的参数。但让我们从更简单的方式开始,使用一个值源。由于有两组值——title和author——我们可以使用一个CsvSource为我们的测试提供这些值。
此外,我们还缺少关键注解:*@ParameterizedTest*。这个注解告诉JUnit我们的测试是参数化的,并且注入了测试值。
让我们尝试快速重构:
@ParameterizedTest
@CsvSource({"Wuthering Heights, Charlotte Bronte", "Jane Eyre, Charlotte Bronte"})
@Test
void givenTitleAndAuthor_whenCreatingBook_thenFieldsArePopulated(String title, String author) {
Book book = new Book(title, author);
assertThat(book.getTitle().length()).isGreaterThan(0);
assertThat(book.getAuthor().length()).isGreaterThan(0);
}
这看起来合理。然而,当我们运行单元测试时,会看到有趣的结果:两个通过的测试运行和一个失败的测试运行。仔细查看,我们还会看到一个警告:
WARNING: Possible configuration error: method [...] resulted in multiple TestDescriptors [org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor, org.junit.jupiter.engine.descriptor.TestTemplateTestDescriptor].
This is typically the result of annotating a method with multiple competing annotations such as @Test, @RepeatedTest, @ParameterizedTest, @TestFactory, etc.
通过添加竞争测试注解,我们无意中创建了多个TestDescriptor。这意味着JUnit仍然在运行原始的*@Test*版本的测试,同时还运行我们的新参数化测试。
简单地移除*@Test*注解可以解决这个问题。
3.3. 使用 ParameterResolver
早些时候,我们讨论了一个简单的ParameterResolver实现示例。现在我们有了一个工作的测试,让我们引入一个BookParameterResolver:
public class BookParameterResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
return parameterContext.getParameter().getType() == Book.class;
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
return parameterContext.getParameter().getType() == Book.class
? new Book("Wuthering Heights", "Charlotte Bronte")
: null;
}
}
这是一个简单的示例,它只为测试提供一个Book实例。既然我们有了一个ParameterResolver来提供测试值,我们应该能够回到第一个示例中的测试。再次尝试:
@Test
void givenTitleAndAuthor_whenCreatingBook_thenFieldsArePopulated(String title, String author) {
Book book = new Book(title, author);
assertThat(book.getTitle().length()).isGreaterThan(0);
assertThat(book.getAuthor().length()).isGreaterThan(0);
}
但是当我们运行这个测试时,相同的异常仍然存在。不过原因略有不同——现在我们有了ParameterResolver,但仍需要告诉JUnit如何使用它。
幸运的是,这只需要在包含测试方法的外部类上添加*@ExtendWith*注解即可:
@ExtendWith(BookParameterResolver.class)
public class BookUnitTest {
@Test
void givenTitleAndAuthor_whenCreatingBook_thenFieldsArePopulated(String title, String author) {
// Test contents...
}
// Other unit tests
}
再次运行,我们看到成功的测试执行。
4. 总结
在这篇文章中,我们讨论了JUnit 5的ParameterResolutionException,以及缺失或冲突的配置如何导致这个异常。如往常一样,文章的所有代码都可以在GitHub上找到。