Writing Templates for Test Cases Using JUnit 5 – 使用JUnit 5为测试用例编写模板

最后修改: 2020年 4月 14日

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

1. Overview

1.概述

The JUnit 5 library offers many new features over its previous versions. One such feature is test templates. In short, test templates are a powerful generalization of JUnit 5’s parameterized and repeated tests.

JUnit 5库比以前的版本提供了许多新功能。其中一个功能是测试模板。简而言之,测试模板是对 JUnit 5 的参数化和重复测试的强大概括。

In this tutorial, we’re going to learn how to create a test template using JUnit 5.

在本教程中,我们将学习如何使用JUnit 5创建一个测试模板。

2. Maven Dependencies

2.Maven的依赖性

Let’s start by adding the dependencies to our pom.xml.

让我们先把依赖项添加到我们的pom.xml中。

We need to add the main JUnit 5  junit-jupiter-engine dependency:

我们需要添加主JUnit 5 junit-jupiter-engine依赖。

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.8.1</version>
</dependency>

In addition to this, we’ll also need to add the junit-jupiter-api dependency:

除此之外,我们还需要添加junit-jupiter-api依赖。

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.8.1</version>
</dependency>

Likewise, we can add the necessary dependencies to our build.gradle file:

同样,我们可以在我们的build.gradle文件中添加必要的依赖项。

testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.8.1'
testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.8.1'

3. The Problem Statement

3.问题陈述

Before looking at test templates, let’s briefly take a look at JUnit 5’s parameterized tests. Parameterized tests allow us to inject different parameters into the test method. As a result, when using parameterized tests, we can execute a single test method multiple times with different parameters.

在看测试模板之前,让我们简单地看看JUnit 5的参数化测试。参数化测试允许我们将不同的参数注入到测试方法中。因此,当使用参数化测试时,我们可以用不同的参数多次执行一个测试方法。

Let’s assume that we would now like to run our test method multiple times — not just with different parameters, but also under a different invocation context each time.

让我们假设,我们现在想多次运行我们的测试方法–不仅仅是用不同的参数,而且每次都是在不同的调用上下文下。

In other words, we would like the test method to be executed multiple times, with each invocation using a different combination of configurations such as:

换句话说,我们希望测试方法能被多次执行,每次调用都使用不同的配置组合,比如说。

  • using different parameters
  • preparing the test class instance differently — that is, injecting different dependencies into the test instance
  • running the test under different conditions, such as enabling/disabling a subset of invocations if the environment is “QA
  • running with a different lifecycle callback behavior — perhaps we want to set up and tear down a database before and after a subset of invocations

Using parameterized tests quickly proves limited in this case. Thankfully, JUnit 5 offers a powerful solution for this scenario in the form of test templates.

在这种情况下,使用参数化测试很快就被证明是有限的。值得庆幸的是,JUnit 5以测试模板的形式为这种情况提供了一个强大的解决方案。

4. Test Templates

4.测试模板

Test templates themselves are not test cases. Instead, as their name suggests, they are just templates for given test cases. They are a powerful generalization of parameterized and repeated tests.

测试模板本身不是测试用例。相反,正如它们的名字所示,它们只是给定测试用例的模板。它们是参数化和重复测试的一个强有力的概括。

Test templates are invoked once for each invocation context provided to them by the invocation context provider(s).

测试模板对于调用上下文提供者提供给它们的每个调用上下文都会被调用一次。

Let’s now look at an example of the test templates. As we established above, the main actors are:

现在让我们看看测试模板的一个例子。正如我们上面所确定的,主要的行为者是。

  • a test target method
  • a test template method
  • one or more invocation context providers registered with the template method
  • one or more invocation contexts provided by each invocation context provider

4.1. The Test Target Method

4.1.测试目标方法

For this example, we’re going to use a simple UserIdGeneratorImpl.generate method as our test target.

在这个例子中,我们将使用一个简单的UserIdGeneratorImpl.generate方法作为我们的测试目标。

Let’s define the UserIdGeneratorImpl class:

我们来定义UserIdGeneratorImpl类。

public class UserIdGeneratorImpl implements UserIdGenerator {
    private boolean isFeatureEnabled;

    public UserIdGeneratorImpl(boolean isFeatureEnabled) {
        this.isFeatureEnabled = isFeatureEnabled;
    }

    public String generate(String firstName, String lastName) {
        String initialAndLastName = firstName.substring(0, 1).concat(lastName);
        return isFeatureEnabled ? "bael".concat(initialAndLastName) : initialAndLastName;
    }
}

The generate method, which is our test target, takes the firstName and lastName as parameters and generates a user id. The format of the user id varies, depending on whether a feature switch is enabled or not.

generate方法,也就是我们的测试目标,将firstNamelastName作为参数,生成一个用户ID。用户ID的格式各不相同,取决于是否启用了一个功能开关。

Let’s see how this looks:

让我们看看这看起来如何。

Given feature switch is disabled When firstName = "John" and lastName = "Smith" Then "JSmith" is returned
Given feature switch is enabled When firstName = "John" and lastName = "Smith" Then "baelJSmith" is returned

Next, let’s write the test template method.

接下来,我们来写一下测试模板方法。

4.2. The Test Template Method

4.2.测试模板法

Here is a test template for our test target method UserIdGeneratorImpl.generate:

这里是我们测试目标方法UserIdGeneratorImpl.generate的测试模板。

public class UserIdGeneratorImplUnitTest {
    @TestTemplate
    @ExtendWith(UserIdGeneratorTestInvocationContextProvider.class)
    public void whenUserIdRequested_thenUserIdIsReturnedInCorrectFormat(UserIdGeneratorTestCase testCase) {
        UserIdGenerator userIdGenerator = new UserIdGeneratorImpl(testCase.isFeatureEnabled());

        String actualUserId = userIdGenerator.generate(testCase.getFirstName(), testCase.getLastName());

        assertThat(actualUserId).isEqualTo(testCase.getExpectedUserId());
    }
}

Let’s take a closer look at the test template method.

让我们仔细看一下测试模板的方法。

To begin with, we create our test template method by marking it with the JUnit 5 @TestTemplate annotation.

首先,我们通过用JUnit 5 @TestTemplate 注解来创建我们的测试模板方法

Following that, we register a context provider, UserIdGeneratorTestInvocationContextProvider, using the @ExtendWith annotation. We can register multiple context providers with the test template. However, for the purpose of this example, we register a single provider.

之后,我们注册一个上下文提供者UserIdGeneratorTestInvocationContextProvider,使用@ExtendWith注解。我们可以在测试模板中注册多个上下文提供者。然而,为了这个例子的目的,我们注册一个提供者。

Also, the template method receives an instance of the UserIdGeneratorTestCase as a parameter. This is simply a wrapper class for the inputs and the expected result of the test case:

另外,模板方法接收一个UserIdGeneratorTestCase的实例作为参数。这只是测试案例的输入和预期结果的一个封装类。

public class UserIdGeneratorTestCase {
    private boolean isFeatureEnabled;
    private String firstName;
    private String lastName;
    private String expectedUserId;

    // Standard setters and getters
}

Finally, we invoke the test target method and assert that that result is as expected

最后,我们调用测试目标方法,并断言该结果符合预期

Now, it’s time to define our invocation context provider.

现在,是时候定义我们的调用上下文提供者了.

4.3. The Invocation Context Provider

4.3.调用语境提供者

We need to register at least one TestTemplateInvocationContextProvider with our test template. Each registered TestTemplateInvocationContextProvider provides a Stream of TestTemplateInvocationContext instances.

我们需要在我们的测试模板中至少注册一个TestTemplateInvocationContextProvider每个注册的TestTemplateInvocationContextProvider提供一个StreamTestTemplateInvocationContext实例

Previously, using the @ExtendWith annotation, we registered UserIdGeneratorTestInvocationContextProvider as our invocation provider.

之前,使用@ExtendWith注解,我们注册了UserIdGeneratorTestInvocationContextProvider作为我们的调用提供者。

Let’s define this class now:

现在我们来定义这个类。

public class UserIdGeneratorTestInvocationContextProvider implements TestTemplateInvocationContextProvider {
    //...
}

Our invocation context implements the TestTemplateInvocationContextProvider interface, which has two methods:

我们的调用上下文实现了TestTemplateInvocationContextProvider接口,它有两个方法。

  • supportsTestTemplate
  • provideTestTemplateInvocationContexts

Let’s start by implementing the supportsTestTemplate method:

让我们从实现supportsTestTemplate方法开始。

@Override
public boolean supportsTestTemplate(ExtensionContext extensionContext) {
    return true;
}

The JUnit 5 execution engine calls the supportsTestTemplate method first to validate if the provider is applicable for the given ExecutionContext. In this case, we simply return true.

JUnit 5执行引擎首先调用supportsTestTemplate方法来验证提供者是否适用于给定的ExecutionContext。在这种情况下,我们只需返回true

Now, let’s implement the provideTestTemplateInvocationContexts method:

现在,让我们实现provideTestTemplateInvocationContexts方法。

@Override
public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
  ExtensionContext extensionContext) {
    boolean featureDisabled = false;
    boolean featureEnabled = true;
 
    return Stream.of(
      featureDisabledContext(
        new UserIdGeneratorTestCase(
          "Given feature switch disabled When user name is John Smith Then generated userid is JSmith",
          featureDisabled,
          "John",
          "Smith",
          "JSmith")),
      featureEnabledContext(
        new UserIdGeneratorTestCase(
          "Given feature switch enabled When user name is John Smith Then generated userid is baelJSmith",
          featureEnabled,
          "John",
          "Smith",
          "baelJSmith"))
    );
}

The purpose of the provideTestTemplateInvocationContexts method is to provide a Stream of TestTemplateInvocationContext instances. For our example, it returns two instances, provided by the methods featureDisabledContext and featureEnabledContext. Consequently, our test template will run twice.

provideTestTemplateInvocationContexts方法的目的是提供一个StreamTestTemplateInvocationContext实例。对于我们的例子,它返回两个实例,由方法featureDisabledContextfeatureEnabledContext提供。因此,我们的测试模板将运行两次。

Next, let’s look at the two TestTemplateInvocationContext instances returned by these methods.

接下来,让我们看看这些方法所返回的两个TestTemplateInvocationContext实例。

4.4. The Invocation Context Instances

4.4.调用上下文实例

The invocation contexts are implementations of the TestTemplateInvocationContext interface and implement the following methods:

调用上下文是TestTemplateInvocationContext接口的实现,并实现以下方法。

  • getDisplayName – provide a test display name
  • getAdditionalExtensions – return additional extensions for the invocation context

Let’s define the featureDisabledContext method that returns our first invocation context instance:

让我们定义featureDisabledContext方法,返回我们的第一个调用环境实例。

private TestTemplateInvocationContext featureDisabledContext(
  UserIdGeneratorTestCase userIdGeneratorTestCase) {
    return new TestTemplateInvocationContext() {
        @Override
        public String getDisplayName(int invocationIndex) {
            return userIdGeneratorTestCase.getDisplayName();
        }

        @Override
        public List<Extension> getAdditionalExtensions() {
            return asList(
              new GenericTypedParameterResolver(userIdGeneratorTestCase), 
              new BeforeTestExecutionCallback() {
                  @Override
                  public void beforeTestExecution(ExtensionContext extensionContext) {
                      System.out.println("BeforeTestExecutionCallback:Disabled context");
                  }
              }, 
              new AfterTestExecutionCallback() {
                  @Override
                  public void afterTestExecution(ExtensionContext extensionContext) {
                      System.out.println("AfterTestExecutionCallback:Disabled context");
                  }
              }
            );
        }
    };
}

Firstly, for the invocation context returned by the featureDisabledContext method, the extensions that we register are:

首先,对于由featureDisabledContext方法返回的调用上下文,我们注册的扩展是。

  • GenericTypedParameterResolver – a parameter resolver extension
  • BeforeTestExecutionCallback – a lifecycle callback extension that runs immediately before the test execution
  • AfterTestExecutionCallback – a lifecycle callback extension that runs immediately after the test execution

However, for the second invocation context, returned by the featureEnabledContext method, let’s register a different set of extensions (keeping the GenericTypedParameterResolver):

然而,对于由featureEnabledContext方法返回的第二个调用上下文,让我们注册一组不同的扩展(保留GenericTypedParameterResolver)。

private TestTemplateInvocationContext featureEnabledContext(
  UserIdGeneratorTestCase userIdGeneratorTestCase) {
    return new TestTemplateInvocationContext() {
        @Override
        public String getDisplayName(int invocationIndex) {
            return userIdGeneratorTestCase.getDisplayName();
        }
    
        @Override
        public List<Extension> getAdditionalExtensions() {
            return asList(
              new GenericTypedParameterResolver(userIdGeneratorTestCase), 
              new DisabledOnQAEnvironmentExtension(), 
              new BeforeEachCallback() {
                  @Override
                  public void beforeEach(ExtensionContext extensionContext) {
                      System.out.println("BeforeEachCallback:Enabled context");
                  }
              }, 
              new AfterEachCallback() {
                  @Override
                  public void afterEach(ExtensionContext extensionContext) {
                      System.out.println("AfterEachCallback:Enabled context");
                  }
              }
            );
        }
    };
}

For the second invocation context, the extensions that we register are:

对于第二个调用环境,我们注册的扩展是。

  • GenericTypedParameterResolver – a parameter resolver extension
  • DisabledOnQAEnvironmentExtension – an execution condition to disable the test if the environment property (loaded from the application.properties file) is “qa
  • BeforeEachCallback – a lifecycle callback extension that runs before each test method execution
  • AfterEachCallback – a lifecycle callback extension that runs after each test method execution

From the above example, it is clear to see that:

从上面的例子中,可以清楚地看到。

  • the same test method is run under multiple invocation contexts
  • each invocation context uses its own set of extensions that differ both in number and nature from the extensions in other invocation contexts

As a result, a test method can be invoked multiple times under a completely different invocation context each time. And by registering multiple context providers, we can provide even more additional layers of invocation contexts under which to run the test.

因此,一个测试方法可以在每次完全不同的调用环境下被多次调用。通过注册多个上下文提供者,我们可以提供更多额外的调用上下文层来运行测试。

5. Conclusion

5.总结

In this article, we looked at how JUnit 5’s test templates are a powerful generalization of parameterized and repeated tests.

在这篇文章中,我们看了JUnit 5的测试模板是如何对参数化和重复测试进行强大的概括。

To begin with, we looked at some limitations of the parameterized tests. Next, we discussed how test templates overcome the limitations by allowing a test to be run under a different context for each invocation.

首先,我们研究了参数化测试的一些限制。接下来,我们讨论了测试模板如何通过允许测试在每次调用的不同上下文下运行来克服这些限制。

Finally, we looked at an example of creating a new test template. We broke the example down to understand how templates work in conjunction with the invocation context providers and invocation contexts.

最后,我们看了一个创建新测试模板的例子。我们分解了这个例子,以了解模板如何与调用上下文提供者和调用上下文一起工作。

As always, the source code for the examples used in this article is available over on GitHub.

一如既往,本文中所使用的示例的源代码可在GitHub上获取