解决 JUnit 5 中的 ParameterResolutionException 异常

1、概览

JUnit 5 引入了一些强大的功能,包括支持参数化测试(parameterized testing)。编写参数化测试可以节省大量时间,而且在许多情况下,只需简单组合注解就能启用参数化测试。

然而,错误配置可能会导致难以调试的异常,例如:ParameterResolutionException

org.junit.jupiter.api.extension.ParameterResolutionException: No ParameterResolver registered for parameter ...

本文将带你了解如何解决 ParameterResolutionException 异常。

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 {
        // 是否支持参数类型的逻辑
    }

    @Override
    public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
        // 参数解析逻辑
    }
}

该类有两个主要方法:

  • supportsParameter():确定是否支持参数类型
  • resolveParameter():返回执行测试的参数

因为 ParameterResolutionsException 是在没有 ParameterResolver 实现的情况下抛出的,所以先不关心实现细节。首先来看看异常的一些潜在原因。

3、ParameterResolutionException

ParameterResolutionException 可能很难调试,尤其是对那些不太熟悉参数化测试的人来说。

首先,义一个简单的 Book 类:

public class Book {
    private String title;
    private String author;
    // Get、Set 方法省略
}

编写一些单元测试,验证 Booktitle 值。

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

不难看出,这两个测试基本上在做同一件事:设置 Booktitle 和检查长度。我们可以将它们合并为一个参数化测试,从而简化测试。

接下来看看这种重构可能出错的几种情况。

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 来提供缺失的参数,但让我们从更简单的 value source 开始。由于有两个值(titleauthor),可以使用 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.

由于添加了冲突的注解,无意中创建了多个 TestDescriptors。这意味着 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) {
        // 测试内容
    }
    // 其他单元测试
}

再次运行,测试成功执行。

4、总结

本文介绍了在 JUnit 5 中导致 ParameterResolutionException 异常的原因以及解决办法。


Ref:https://www.baeldung.com/junit-5-parameterresolutionexception