Solving the ParameterResolutionException in JUnit 5 – 解决 JUnit 5 中的 ParameterResolutionException 问题

最后修改: 2024年 1月 14日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

JUnit 5 introduced some powerful features, including support for parameterized testing. Writing parameterized tests can save a lot of time, and in many cases, they can be enabled with a simple combination of annotations.

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

However, incorrect configuration can lead to exceptions that are difficult to debug since JUnit manages many aspects of test execution behind the scenes.

但是,不正确的配置可能会导致难以调试的异常,因为 JUnit 在幕后管理测试执行的许多方面

One such exception is the ParameterResolutionException:

其中一个例外就是 ParameterResolutionException

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

In this tutorial, we’ll explore the causes of this exception and how to solve it.

在本教程中,我们将探讨产生这种异常的原因以及解决方法。

2. JUnit 5’s ParameterResolver 

2. JUnit 5 的参数解析器</em

To understand the cause of this exception, we first need to understand what the message is telling us we’re missing: a ParameterResolver.

要了解该异常的原因,我们首先需要了解该消息告诉我们缺少了什么:一个 ParameterResolver

In JUnit 5, the ParameterResolver interface was introduced to allow developers to extend JUnit’s basic functionality and write tests that take parameters of any type. Let’s look at a simple ParameterResolver implementation:

在 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
    }
}

We can see that the class has two main methods:

我们可以看到,该类有两个主要方法:

  • supportsParameter(): determines if a parameter type is supported
  • resolveParameter(): returns a parameter for test execution

Because the ParameterResolutionException is thrown in the absence of a ParameterResolver implementation, we won’t get too concerned with implementation details just yet. Let’s first discuss some potential causes of the exception.

由于 ParameterResolutionsException 是在没有 ParameterResolver 实现的情况下抛出的,因此我们暂时不会过多关注实现细节。让我们先讨论一下异常的一些潜在原因。

3. The ParameterResolutionException

3.参数解析异常</em

The ParameterResolutionException can be difficult to debug, especially for those less familiar with parameterized testing.

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

To start, let’s define a simple Book class that we’ll write unit tests for:

首先,让我们定义一个简单的 Book 类,并为其编写单元测试:

public class Book {
    private String title;
    private String author;
    // Standard getters and setters
}

For our example, we’ll write some unit tests for Book that verify different title values. Let’s start with two very simple tests:

在我们的示例中,我们将为 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);
}

It’s easy to see these two tests are basically doing the same thing: setting the Book title and checking the length. We can simplify the tests by combining them into a single parameterized test. Let’s discuss some ways in which this refactoring could go wrong.

不难看出,这两个测试基本上在做同一件事:设置 Book 标题并检查长度。我们可以将它们合并为一个参数化测试,从而简化测试。让我们讨论一下这种重构可能出错的几种方式。

3.1. Passing Parameters to @Test Methods

3.1.向 @Test 方法传递参数

Taking a very quick approach, we may believe passing parameters to the @Test annotated method is enough:

我们可以采取一种非常快速的方法,认为将参数传递给 @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);
}

The code compiles and runs, but thinking about this a little further, we should question where these parameters are coming from. Running this example, we see an exception:

代码可以编译和运行,但再仔细想想,我们就会发现这些参数是从哪里来的。在运行这个示例时,我们看到了一个异常:

org.junit.jupiter.api.extension.ParameterResolutionException: No ParameterResolver registered for parameter [java.lang.String arg0] in method ...

JUnit has no way to know what parameters to pass to the test method.

JUnit 无法知道向测试方法传递哪些参数

Let’s continue refactoring our unit test and look into another cause of the ParameterResolutionException.

让我们继续重构单元测试,看看 ParameterResolutionException. 的另一个原因。

3.2. Competing Annotations

3.2.竞争性注释

We could supply the missing parameters with a ParameterResolver as we mentioned earlier, but let’s start more simply with a value source. Since there are two values — title and author — we can use a CsvSource to provide these values to our test.

我们可以使用前面提到的 ParameterResolver 来提供缺失的参数,但让我们从 值源 开始更简单一些。由于有两个值 – titleauthor – 我们可以使用 CsvSource 将这些值提供给我们的测试。

Additionally, we’re missing a key annotation: @ParameterizedTest. This annotation informs JUnit that our test is parameterized and has test values injected into it.

此外,我们还缺少一个关键注解:@ParameterizedTest.该注解通知 JUnit 我们的测试已被参数化并注入了测试值。

Let’s make a quick attempt at refactoring:

让我们快速尝试一下重构:

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

This seems reasonable. However, when we run the unit test, we see something interesting: two passing test runs and a third failing test run. Looking closer, we see a warning as well:

这似乎是合理的。然而,当我们运行单元测试时,我们发现了一些有趣的现象:两次测试运行通过,第三次测试运行失败。仔细观察,我们还发现了一个警告:

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.

By adding competing test annotations, we’ve unintentionally created multiple TestDescriptors. What this means is that JUnit is still running the original @Test version of our test along with our new parameterized test.

通过添加相互竞争的测试注释,我们无意中创建了多个 TestDescriptors 。这意味着 JUnit 仍在运行测试的原始 @Test 版本和新的参数化测试。

Simply removing the @Test annotation fixes this issue.

只需移除@Test注释即可解决这一问题

3.3. Working with a ParameterResolver

3.3.与 ParameterResolver 一起工作

Earlier, we discussed a simple example of a ParameterResolver implementation. Now that we have a working test, let’s introduce a BookParameterResolver:

前面,我们讨论了 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;
    }
}

This is a simple example that just returns a single Book instance for testing. Now that we have a ParameterResolver to provide us with test values, we should be able to go back to the test from our first example. Again, we may try:

这是一个简单的示例,只返回一个 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);
}

But as we see when running this test, the same exception persists. The cause is slightly different though — now that we have a ParameterResolver, we still need to tell JUnit how to use it.

但是,正如我们在运行该测试时所看到的,同样的异常仍然存在。但原因略有不同–既然我们已经有了一个 ParameterResolver, 我们仍然需要告诉 JUnit 如何使用它

Fortunately, this is as simple as adding the @ExtendWith annotation to the outer class that contains our test methods:

幸运的是,这就像在包含测试方法的外层类中添加 @ExtendWith 注解一样简单:

@ExtendWith(BookParameterResolver.class)
public class BookUnitTest {
    @Test
    void givenTitleAndAuthor_whenCreatingBook_thenFieldsArePopulated(String title, String author) {
        // Test contents...
    }
    // Other unit tests
}

Running this again, we see a successful test execution.

再次运行,我们看到测试执行成功。

4. Conclusion

4.结论

In this article, we discussed JUnit 5’s ParameterResolutionException and how missing or competing configurations can cause this exception. As always, all of the code for the article can be found over on GitHub.

在本文中,我们讨论了 JUnit 5 的 ParameterResolutionException 以及缺失或竞争配置如何导致该异常。一如既往,本文的所有代码均可在 GitHub 上找到