解决JUnit 5中的ParameterResolutionException

2025/03/15

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编写一些单元测试来验证不同的title值,让我们从两个非常简单的测试开始:

@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);
}

很容易看出这两个测试基本上在做同样的事情:设置图书title并检查长度。我们可以将它们合并为一个参数化测试来简化测试。让我们讨论一下这种重构可能出错的一些方式。

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以及缺失或竞争的配置如何导致此异常。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章