Inject Parameters into JUnit Jupiter Unit Tests – 向JUnit Jupiter单元测试注入参数

最后修改: 2017年 8月 10日

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

1. Overview

1.概述

Prior to JUnit 5, to introduce a cool new feature, the JUnit team would have to do it to the core API. With JUnit 5 the team decided it was time to push the capability to extend the core JUnit API outside of JUnit itself, a core JUnit 5 philosophy called “prefer extension points over features”.

在JUnit 5之前,要引入一个很酷的新功能,JUnit团队必须在核心API上做。在 JUnit 5 中,团队决定是时候将扩展核心 JUnit API 的能力推到 JUnit 本身之外了,这是 JUnit 5 的核心理念,称为”优先于功能的扩展点“。

In this article, we’re going to focus on one of those extension point interfaces – ParameterResolver – that you can use to inject parameters into your test methods. There are a couple of different ways to make the JUnit platform aware of your extension (a process known as “registration”), and in this article, we’ll focus on declarative registration (i.e., registration via source code).

在这篇文章中,我们将重点讨论其中的一个扩展点接口–ParameterResolver –你可以用它来向你的测试方法注入参数。有几种不同的方法可以让JUnit平台意识到你的扩展(这个过程被称为 “注册”),在这篇文章中,我们将重点讨论声明式注册(即通过源代码注册)。

2. ParameterResolver

2.ParameterResolver

Injecting parameters into your test methods could be done using the JUnit 4 API, but it was fairly limited. With JUnit 5, the Jupiter API can be extended – by implementing ParameterResolver – to serve up objects of any type to your test methods. Let’s have a look.

在测试方法中注入参数可以使用JUnit 4的API来完成,但这是相当有限的。在JUnit 5中,Jupiter API可以被扩展–通过实现ParameterResolver –将任何类型的对象提供给你的测试方法。让我们来看看。

2.1. FooParameterResolver

2.1.FooParameterResolver

public class FooParameterResolver implements ParameterResolver {
  @Override
  public boolean supportsParameter(ParameterContext parameterContext, 
    ExtensionContext extensionContext) throws ParameterResolutionException {
      return parameterContext.getParameter().getType() == Foo.class;
  }

  @Override
  public Object resolveParameter(ParameterContext parameterContext, 
    ExtensionContext extensionContext) throws ParameterResolutionException {
      return new Foo();
  }
}

First, we need to implement ParameterResolver – which has two methods:

首先,我们需要实现ParameterResolver –,它有两个方法。

  • supportsParameter() – returns true if the parameter’s type is supported (Foo in this example), and
  • resolveParamater() – serves up an object of the correct type (a new Foo instance in this example), which will then be injected in your test method

2.2. FooTest

2.2. FooTest

@ExtendWith(FooParameterResolver.class)
public class FooTest {
    @Test
    public void testIt(Foo fooInstance) {
        // TEST CODE GOES HERE
    }  
}

Then to use the extension, we need to declare it – i.e., tell the JUnit platform about it – via the @ExtendWith annotation (Line 1).

然后,为了使用这个扩展,我们需要通过@ExtendWith注解来声明它–即告诉JUnit平台它的情况(第1行)。

When the JUnit platform runs your unit test, it will get a Foo instance from FooParameterResolver and pass it to the testIt() method (Line 4).

当JUnit平台运行你的单元测试时,它将从FooParameterResolver获得一个Foo实例,并将其传递给testIt()方法(第4行)。

The extension has a scope of influence, which activates the extension, depending on where it’s declared.

扩展有一个影响范围,它可以激活扩展,这取决于哪里它被声明。

The extension may either be active at the:

分机可以在……活动。

  • method level, where it is active for just that method, or
  • class level, where it is active for the entire test class, or @Nested test class as we’ll soon see

Note: you should not declare a ParameterResolver at both scopes for the same parameter type, or the JUnit Platform will complain about this ambiguity.

注意:你不应该在两个作用域都声明一个ParameterResolver针对同一参数类型,否则JUnit平台将抱怨这种模糊性

For this article, we’ll see how to write and use two extensions to inject Person objects: one that injects “good” data (called ValidPersonParameterResolver) and one that injects “bad” data (InvalidPersonParameterResolver). We’ll use this data to unit test a class called PersonValidator, which validates the state of a Person object.

在这篇文章中,我们将看到如何编写和使用两个扩展来注入Person对象:一个是注入 “好 “数据(称为ValidPersonParameterResolver),一个是注入 “坏 “数据(InvalidPersonParameterResolver)。我们将使用这些数据对一个名为PersonValidator的类进行单元测试,该类验证Person对象的状态。

3. Write the Extensions

3.编写扩展程序

Now that we understand what a ParameterResolver extension is, we’re ready to write:

现在我们明白了什么是ParameterResolver扩展,我们就可以开始写了。

  • one which provides valid Person objects (ValidPersonParameterResolver), and
  • one which provides invalid Person objects (InvalidPersonParameterResolver)

3.1. ValidPersonParameterResolver

3.1.ValidPersonParameterResolver

public class ValidPersonParameterResolver implements ParameterResolver {

  public static Person[] VALID_PERSONS = {
      new Person().setId(1L).setLastName("Adams").setFirstName("Jill"),
      new Person().setId(2L).setLastName("Baker").setFirstName("James"),
      new Person().setId(3L).setLastName("Carter").setFirstName("Samanta"),
      new Person().setId(4L).setLastName("Daniels").setFirstName("Joseph"),
      new Person().setId(5L).setLastName("English").setFirstName("Jane"),
      new Person().setId(6L).setLastName("Fontana").setFirstName("Enrique"),
  };

Notice the VALID_PERSONS array of Person objects. This is the repository of valid Person objects from which one will be chosen at random each time the resolveParameter() method is called by the JUnit platform.

注意VALID_PERSONS数组的Person对象。这是有效的Person对象的存储库,每次JUnit平台调用resolveParameter()方法时,都会从中随机选择一个。

Having the valid Person objects here accomplishes two things:

在这里有有效的Person对象,可以完成两件事。

  1. Separation of concerns between the unit test and the data that drives it
  2. Reuse, should other unit tests require valid Person objects to drive them
@Override
public boolean supportsParameter(ParameterContext parameterContext, 
  ExtensionContext extensionContext) throws ParameterResolutionException {
    boolean ret = false;
    if (parameterContext.getParameter().getType() == Person.class) {
        ret = true;
    }
    return ret;
}

If the type of parameter is Person, then the extension tells the JUnit platform that it supports that parameter type, otherwise it returns false, saying it does not.

如果参数的类型是Person,那么该扩展告诉JUnit平台它支持该参数类型,否则它返回false,表示它不支持。

Why should this matter? While the examples in this article are simple, in a real-world application, unit test classes can be very large and complex, with many test methods that take different parameter types. The JUnit platform must check with all registered ParameterResolvers when resolving parameters within the current scope of influence.

为什么这很重要?虽然本文中的例子很简单,但在真实世界的应用中,单元测试类可能非常庞大和复杂,有许多采取不同参数类型的测试方法。JUnit平台在解析参数时必须与所有注册的ParameterResolvers进行检查,在当前影响范围内

@Override
public Object resolveParameter(ParameterContext parameterContext, 
  ExtensionContext extensionContext) throws ParameterResolutionException {
    Object ret = null;
    if (parameterContext.getParameter().getType() == Person.class) {
        ret = VALID_PERSONS[new Random().nextInt(VALID_PERSONS.length)];
    }
    return ret;
}

A random Person object is returned from the VALID_PERSONS array. Note how resolveParameter() is only called by the JUnit platform if supportsParameter() returns true.

一个随机的Person对象将从VALID_PERSONS数组中返回。请注意,只有当supportsParameter()返回true时,JUnit平台才会调用resolveParameter()

3.2. InvalidPersonParameterResolver

3.2.InvalidPersonParameterResolver

public class InvalidPersonParameterResolver implements ParameterResolver {
  public static Person[] INVALID_PERSONS = {
      new Person().setId(1L).setLastName("Ad_ams").setFirstName("Jill,"),
      new Person().setId(2L).setLastName(",Baker").setFirstName(""),
      new Person().setId(3L).setLastName(null).setFirstName(null),
      new Person().setId(4L).setLastName("Daniel&").setFirstName("{Joseph}"),
      new Person().setId(5L).setLastName("").setFirstName("English, Jane"),
      new Person()/*.setId(6L).setLastName("Fontana").setFirstName("Enrique")*/,
  };

Notice the INVALID_PERSONS array of Person objects. Just like with ValidPersonParameterResolver, this class contains a store of “bad” (i.e., invalid) data for use by unit tests to ensure, for example, that PersonValidator.ValidationExceptions are properly thrown in the presence of invalid data:

请注意INVALID_PERSONS数组的Person对象。就像ValidPersonParameterResolver一样,这个类包含了一个 “坏”(即无效)数据的存储,供单元测试使用,以确保,例如,PersonValidator.ValidationExceptions在出现无效数据时被正确抛出。

@Override
public Object resolveParameter(ParameterContext parameterContext, 
  ExtensionContext extensionContext) throws ParameterResolutionException {
    Object ret = null;
    if (parameterContext.getParameter().getType() == Person.class) {
        ret = INVALID_PERSONS[new Random().nextInt(INVALID_PERSONS.length)];
    }
    return ret;
}

@Override
public boolean supportsParameter(ParameterContext parameterContext, 
  ExtensionContext extensionContext) throws ParameterResolutionException {
    boolean ret = false;
    if (parameterContext.getParameter().getType() == Person.class) {
        ret = true;
    }
    return ret;
}

The rest of this class naturally behaves exactly like its “good” counterpart.

这个类别的其他部分自然表现得与它的 “好 “对应部分完全一样。

4. Declare and Use the Extensions

4.声明和使用扩展

Now that we have two ParameterResolvers, it’s time to put them to use. Let’s create a JUnit test class for PersonValidator called PersonValidatorTest.

现在我们有了两个ParameterResolvers,是时候将它们投入使用了。让我们为PersonValidator创建一个JUnit测试类,称为PersonValidatorTest

We’ll be using several features available only in JUnit Jupiter:

我们将使用仅在JUnit Jupiter中可用的几个特性。

  • @DisplayName – this is the name that shows up on test reports, and much more human readable
  • @Nested – creates a nested test class, complete with its own test lifecycle, separate from its parent class
  • @RepeatedTest – the test is repeated the number of times specified by the value attribute (10 in each example)

By using @Nested classes, we’re able to test both valid and invalid data in the same test class, while at the same time keeping them completely sandboxed away from each other:

通过使用@Nested类,我们能够在同一个测试类中同时测试有效和无效的数据,同时让它们完全远离沙盒。

@DisplayName("Testing PersonValidator")
public class PersonValidatorTest {

    @Nested
    @DisplayName("When using Valid data")
    @ExtendWith(ValidPersonParameterResolver.class)
    public class ValidData {
        
        @RepeatedTest(value = 10)
        @DisplayName("All first names are valid")
        public void validateFirstName(Person person) {
            try {
                assertTrue(PersonValidator.validateFirstName(person));
            } catch (PersonValidator.ValidationException e) {
                fail("Exception not expected: " + e.getLocalizedMessage());
            }
        }
    }

    @Nested
    @DisplayName("When using Invalid data")
    @ExtendWith(InvalidPersonParameterResolver.class)
    public class InvalidData {

        @RepeatedTest(value = 10)
        @DisplayName("All first names are invalid")
        public void validateFirstName(Person person) {
            assertThrows(
              PersonValidator.ValidationException.class, 
              () -> PersonValidator.validateFirstName(person));
        }
    }
}

Notice how we’re able to use the ValidPersonParameterResolver and InvalidPersonParameterResolver extensions within the same main test class – by declaring them only at the @Nested class level. Try that with JUnit 4! (Spoiler alert: you can’t do it!)

请注意我们是如何在同一个主测试类中使用ValidPersonParameterResolverInvalidPersonParameterResolver扩展的–只需在@Nested类级别声明它们。用JUnit 4试试吧!(预告:你做不到!)

5. Conclusion

5.总结

In this article, we explored how to write two ParameterResolver extensions – to serve up valid and invalid objects. Then we had a look at how to use these two ParameterResolver implementations in a unit test.

在这篇文章中,我们探讨了如何编写两个ParameterResolver扩展–以提供有效和无效的对象。然后我们看了一下如何在单元测试中使用这两个ParameterResolver实现。

As always, the code is available over on Github.

像往常一样,代码可以在Github上获得

And, if you want to learn more about the JUnit Jupiter extension model, check out the JUnit 5 User’s Guide, or part 2 of my tutorial on developerWorks.

而且,如果您想进一步了解 JUnit Jupiter 扩展模型,请查看JUnit 5 用户指南,或我关于 developerWorks 的教程的第二部分