1. 概述

在JUnit 5之前,要引入新功能,JUnit团队必须修改核心API。而JUnit 5团队决定将扩展核心API的能力移出JUnit本身,这体现了JUnit 5的核心哲学——“优先扩展点而非功能”。

本文将聚焦其中一个扩展点接口——ParameterResolver,你可以用它向测试方法注入参数。注册扩展(让JUnit平台感知扩展的过程)有几种方式,本文重点介绍声明式注册(即通过源代码注册)。

2. ParameterResolver

JUnit 4也能向测试方法注入参数,但功能相当有限。JUnit 5的Jupiter API可通过实现ParameterResolver进行扩展,为测试方法提供任意类型的对象。我们来看看具体实现。

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

首先需要实现ParameterResolver接口,它包含两个方法:

  • supportsParameter() - 当参数类型受支持时返回true(本例中为Foo类型)
  • resolveParameter() - 提供正确类型的对象(本例中返回新的Foo实例),该对象将被注入测试方法

2.2. FooTest

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

使用扩展时需通过*@ExtendWith*注解声明(第1行),让JUnit平台感知它的存在。

当JUnit平台运行单元测试时,会从FooParameterResolver获取Foo实例并传递给*testIt()*方法(第4行)。

扩展有作用域,根据声明位置激活扩展:

  • 方法级:仅对当前方法生效
  • 类级:对整个测试类或*@Nested*测试类生效(后续会看到)

⚠️ 注意:不要在同一参数类型的两个作用域同时声明ParameterResolver,否则JUnit平台会因歧义报错。

本文将编写两个扩展来注入Person对象:一个注入“有效”数据(ValidPersonParameterResolver),一个注入“无效”数据(InvalidPersonParameterResolver)。用这些数据测试PersonValidator类,验证Person对象的状态。

3. 编写扩展

理解了ParameterResolver扩展后,现在编写:

  • 提供有效Person对象的扩展(ValidPersonParameterResolver
  • 提供无效Person对象的扩展(InvalidPersonParameterResolver

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"),
  };

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

这样做有两个好处:

  1. 分离单元测试与驱动数据
  2. 复用性:其他需要有效Person对象的单元测试可直接使用
@Override
public boolean supportsParameter(ParameterContext parameterContext, 
  ExtensionContext extensionContext) throws ParameterResolutionException {
    boolean ret = false;
    if (parameterContext.getParameter().getType() == Person.class) {
        ret = true;
    }
    return ret;
}

当参数类型为Person时,扩展告知JUnit平台支持该类型,否则返回false。

为什么需要这个判断?虽然本文示例简单,但实际应用中单元测试类可能庞大复杂,包含多种参数类型的测试方法。JUnit平台解析参数时,必须检查当前作用域内所有注册的ParameterResolver

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

VALID_PERSONS数组返回随机Person对象。注意:resolveParameter()仅在supportsParameter()返回true时被调用。

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")*/,
  };

注意INVALID_PERSONS数组——与ValidPersonParameterResolver类似,这里存储“坏数据”(无效数据)。单元测试可用这些数据验证PersonValidator在遇到无效数据时是否正确抛出ValidationException

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

其余部分与“有效数据”扩展行为完全一致。

4. 声明并使用扩展

现在有两个ParameterResolver,是时候派上用场了。为PersonValidator创建JUnit测试类PersonValidatorTest

我们将使用JUnit Jupiter的几个特性:

  • @DisplayName - 测试报告显示的名称,更易读
  • @Nested - 创建嵌套测试类,拥有独立的生命周期
  • @RepeatedTest - 按指定次数重复测试(示例中为10次)

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

✅ 关键点:通过仅在*@Nested类级别声明扩展,我们在同一主测试类中同时使用了ValidPersonParameterResolverInvalidPersonParameterResolver*。试试用JUnit 4实现这个功能?(剧透:做不到!)

5. 总结

本文探讨了如何编写两个ParameterResolver扩展——分别提供有效和无效对象。然后展示了如何在单元测试中使用这两个实现。

完整代码请访问GitHub仓库

想深入了解JUnit Jupiter扩展模型?可参考:


原始标题:Inject Parameters into JUnit Jupiter Unit Tests

» 下一篇: NoException 介绍