一、问题描述

在使用 Junit 5 写 Spring Boot 项目测试用例的时候,发现一个问题:使用构造器注入 Service 类的时候,执行测试用例时,会显示如下异常:

1
2
3
4
5
org.junit.jupiter.api.extension.ParameterResolutionException: No ParameterResolver registered for parameter [cn.z2huo.demo.mapper.user.UserMapper arg0] in constructor [public cn.z2huo.demo.mapper.user.UserMapperTest(cn.z2huo.demo.mapper.user.UserMapper)].

at java.base/java.util.Optional.orElseGet(Optional.java:364)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)

经过测试 UserMapper 是已经注册到容器中的,并且在 main 中使用构造器注入是可以获取到 UserMapper 的,但是在 test 中单纯使用构造器注入却获取不到,而使用带有 @Autowired 的属性注入或带有 @Autowired 的构造器注入是可以获取到的。猜测是在使用 spring 中使用 junit 5 的问题。

另外,几种装配 Bean 的方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Slf4j  
@SpringBootTest(classes = MapperMybatisPlusTestApplication.class)
class UserMapperTest {

// 注入失败
private final UserMapper userMapper;

public UserMapperTest(UserMapper userMapper) {
this.userMapper = userMapper;
}
}

@Slf4j
@SpringBootTest(classes = MapperMybatisPlusTestApplication.class)
class UserMapperTest {

// 注入成功
@Autowired
private UserMapper userMapper;
}

@Slf4j
@SpringBootTest(classes = MapperMybatisPlusTestApplication.class)
class UserMapperTest {

// 注入成功
private final UserMapper userMapper;

@Autowired
public UserMapperTest(UserMapper userMapper) {
this.userMapper = userMapper;
}
}

二、排查问题

从异常 ParameterResolutionException 入手,定位问题,具体报错位置为 org.junit.jupiter.engine.execution.ParameterResolutionUtils#resolveParameter 方法,该方法中有如下判断:

1
2
3
4
5
6
7
8
List<ParameterResolver> matchingResolvers = extensionRegistry.stream(ParameterResolver.class)
.filter(resolver -> resolver.supportsParameter(parameterContext, extensionContext))
.collect(toList());

if (matchingResolvers.isEmpty()) {
throw new ParameterResolutionException(String.format("No ParameterResolver registered for parameter [%s] in %s [%s].",
parameterContext.getParameter(), asLabel(executable), executable.toGenericString()));
}

这段代码会检查 ParameterResolver 是否支持为提供的 ExtensionContext 中的 ParameterContext 所指定的参数解析实参。

Spring + Junit 5 需要使用 @SpringBootTest 注解,或者添加 SpringExtension 扩展,SpringExtension 扩展实现了 ParameterResolver 接口,SpringExtension 中的 supportsParameter 方法如下:

1
2
3
4
5
6
7
8
9
10
11
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
Parameter parameter = parameterContext.getParameter();
Executable executable = parameter.getDeclaringExecutable();
Class<?> testClass = extensionContext.getRequiredTestClass();
PropertyProvider junitPropertyProvider = propertyName ->
extensionContext.getConfigurationParameter(propertyName).orElse(null);
return (TestConstructorUtils.isAutowirableConstructor(executable, testClass, junitPropertyProvider) ||
ApplicationContext.class.isAssignableFrom(parameter.getType()) ||
supportsApplicationEvents(parameterContext) ||
ParameterResolutionDelegate.isAutowirable(parameter, parameterContext.getIndex()));
}

SpringExtension#supportsParameter 方法确定提供的 ParameterContext 中的参数值是否应从测试的 ApplicationContext 中自动装配。

如果满足以下任一条件,则认为参数是可以自动装配的:

  • 声明该参数的可执行文件是构造函数,并且 TestConstructorUtils.isAutowirableConstructor(Constructor, Class, PropertyProvider) 返回 true。注意,isAutowirableConstructor() 将使用一个回退的 PropertyProvider 调用,该提供者将其查找委托给 ExtensionContext.getConfigurationParameter(String)
  • 参数类型为 ApplicationContext 或其子类型。
  • 参数类型为 ApplicationEvents 或其子类型。
  • ParameterResolutionDelegate.isAutowirable 返回 true

警告:如果测试类的构造函数被 @Autowired 注解或自动装配(参见 @TestConstructor),Spring 将负责解析构造函数中的所有参数。因此,其他注册的 ParameterResolver 将无法解析这些参数。

可以看到,报错的直接原因就是 SpringExtension#supportsParameter 方法的返回结果为 false,并且因为是构造函数自动装配 Bean 的问题,该判断中这块功能的判断就是 TestConstructorUtils.isAutowirableConstructor(executable, testClass, junitPropertyProvider) 方法。

isAutowirableConstructor 方法用来确定给定测试类的提供的可执行文件是否是可自动装配的构造函数。如果提供的可执行文件是构造函数,则此方法会委托给 isAutowirableConstructor(Constructor, Class, PropertyProvider) 进行判断;否则返回 false

isAutowirableConstructor 重载方法内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static boolean isAutowirableConstructor(Constructor<?> constructor, Class<?> testClass,
@Nullable PropertyProvider fallbackPropertyProvider) {

// Is the constructor annotated with @Autowired/@Inject?
if (isAnnotatedWithAutowiredOrInject(constructor)) {
return true;
}

AutowireMode autowireMode;

// Is the test class annotated with @TestConstructor?
TestConstructor testConstructor = TestContextAnnotationUtils.findMergedAnnotation(testClass, TestConstructor.class);
if (testConstructor != null) {
autowireMode = testConstructor.autowireMode();
}
else {
// Custom global default from SpringProperties?
String value = SpringProperties.getProperty(TestConstructor.TEST_CONSTRUCTOR_AUTOWIRE_MODE_PROPERTY_NAME);
autowireMode = AutowireMode.from(value);

// Use fallback provider?
if (autowireMode == null && fallbackPropertyProvider != null) {
value = fallbackPropertyProvider.get(TestConstructor.TEST_CONSTRUCTOR_AUTOWIRE_MODE_PROPERTY_NAME);
autowireMode = AutowireMode.from(value);
}
}

return (autowireMode == AutowireMode.ALL);
}

该方法会判断构造方法是否支持自动装配,方法中有两个分支,一个是判断 @TestConstructor 注解,另一个是去寻找 spring properties 中的 spring.test.constructor.autowire.mode 配置。

其中涉及到的枚举如下:

1
2
3
4
5
6
enum AutowireMode {

ALL,

ANNOTATED;
}

AutowireMode 枚举类中的两个成员含义如下:

  • ALL,所有测试构造函数的参数都将被自动装配,就好像该构造函数本身被注解了 @Autowired@jakarta.inject.Inject@javax.inject.Inject 一样。
  • ANNOTATED,每个单独的测试构造函数参数只有在被 @Autowired@Qualifier@Value 注解时,或者当构造函数本身被 @Autowired@jakarta.inject.Inject@javax.inject.Inject 注解时,才会被自动装配。

1、解决方案一

添加 TestConstructor 注解:

1
2
3
4
5
6
7
8
@Slf4j  
@MapperScan(basePackages = {"cn.z2huo.demo.mapper"})
@SpringBootTest(classes = MapperMybatisPlusTestApplication.class)
@RequiredArgsConstructor
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
class UserMapperTest {

}

2、解决方案二

添加 spring 属性,在 resources 目录下新建文件 spring.properties 并在其中添加如下配置:

1
spring.test.constructor.autowire.mode=ALL

三、其他

1、Spring 官方文档对 TestConstructor 注解的描述

@TestConstructor 是一个可以应用于测试类的注解,用于配置测试类构造函数的参数如何从测试的 ApplicationContext 中的组件进行自动装配。

如果测试类上没有 @TestConstructor 或者元注解形式的 @TestConstructor,则会使用默认的测试构造函数自动装配模式。然而,需要注意的是,构造函数上的本地声明 @Autowired@jakarta.inject.Inject@javax.inject.Inject 优先于 @TestConstructor 和默认模式。

@TestConstructor 仅在与 SpringExtension 结合使用时支持,适用于 JUnit Jupiter。请注意,SpringExtension 经常会自动为你注册——例如,当你使用 @SpringJUnitConfig@SpringJUnitWebConfig 注解或 Spring Boot Test 中的各种测试相关注解时。

1.1 更改默认测试构造函数自动装配模式

可以通过设置 JVM 系统属性 spring.test.constructor.autowire.modeall 来更改默认的测试构造函数自动装配模式。添加如下参数:-Dspring. test. constructor. autowire. mode=all

或者,也可以通过 SpringProperties 机制来设置默认模式。

此外,默认模式还可以配置为 JUnit 平台的配置参数。

如果未设置 spring.test.constructor.autowire.mode 属性,则测试类的构造函数将不会被自动装配。

相关链接

Spring JUnit Jupiter Testing Annotations :: Spring Framework

OB tags

#Junit #Spring #Junit5