前言
之前我写过两篇关于JUnit
5参数化测试的文章,而今将要提到的模板测试其实跟参数化测试也有异曲同工之处。在JUnit
5中,如果你使用了Spring
框架,那么方法参数支持依赖注入,事实上,就算没有使用Spring
,这些JUnit
类型:
TestInfo
: 测试方法的上下文信息;如果方法被@Test
、@RepeatedTest
、@ParameterizedTest
、@TestFactory
、@BeforeEach
、@AfterEach
、@BeforeAll
以及@AfterAll
标注,那么可在方法参数使用,参见TestInfoParameterResolver
TestReporter
:参数类型;如果方法被@BeforeEach
、@AfterEach
以及@Test
标注,那么可在方法参数中使用,参见TestReporterParameterResolver
RepetitionInfo
:循环的相关信息;用在会循环执行的方法上,即被标注@RepeatedTest
、@BeforeEach
、@AfterEach
的方法上, 参见RepetitionInfoParameterResolver
如果你写到相应测试方法的参数上,JUnit
都会自动注入,但是除此之外的类型基本都会报错了,除了使用参数化测试之外,另一种方案就是模板测试了。
模板测试
被@TestTemplate
标注的方法即表示这是一个模板方法,模板很好理解,就是写好执行逻辑,让调用者传入不同参数反复去调用即可,这简直像极了参数化测试!你可以以参数化测试的思维来看待它们,但在某种场景下,模板测试或许比参数化使用起来更加高效。
@TestTemplate
这个注解是模板测试中最重要的两个注解之一(另一个是@ExtendWith
),在声明周期回调和扩展方面,模板测试方法跟普通@Test
测试方法是一致的,并且它的返回必须是void
,不能被private
或static
修饰:
@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Testable
public @interface TestTemplate {
}
透过源码可知这只是个标记注解,与@Test
不同,它不能单独使用,需要注册一个TestTemplateInvocationContextProvider
类型:
public interface TestTemplateInvocationContextProvider extends Extension {
// 可根据context上下文信息决定是否是支持的参数类型,如果为false那么
// provideTestTemplateInvocationContexts方法不会被执行
// ExtensionContext是标注@ExtendWith注解的上下文信息,稍后讲述
boolean supportsTestTemplate(ExtensionContext context);
Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(ExtensionContext context);
}
这个接口的主要工作是构造并返回TestTemplateInvocationContext
流,看定义:
public interface TestTemplateInvocationContext {
// 每执行一次模板方法都会被调用,定义显示名称,一般不用复写。
default String getDisplayName(int invocationIndex) {
return "[" + invocationIndex + "]";
}
// 核心!参数的生成逻辑,必须复写
default List<Extension> getAdditionalExtensions() {
return emptyList(); // java.util.Collections.emptyList
}
}
上面出现的Extension
接口其实空无一物,它更多是作为父接口统合其子类型的存在:
public interface Extension {
}
有必要看看其继承树,方便在使用时参考:
@ExtendWith
这个注解用于在JUnit
注册扩展,它在模板测试中必不可少的存在,前面提到我们需要注册TestTemplateInvocationContextProvider
,这个任务就是通过@ExtendWith
(可重复注解,多个时结合@Extensions
使用)完成的(当然不仅限于模板测试):
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Repeatable(Extensions.class)
public @interface ExtendWith {
Class<? extends Extension>[] value();
}
创建模板方法
所有部件都已齐备,可以创建模板方法了,假定我们有个方法:
void test(Integer val1, Integer val2) {
System.err.println("value 1: " + val1 + " || value 2: " + val2);
}
方法中有两个Integer
参数,如果使用@Test
注解标注肯定是会报错的,JUnit
不知道如何给这些参数填值。既然模板方法必须要一个TestTemplateInvocationContextProvider
,我们就先在测试类中提供一个实现(注意实现类必须是static
,如果你提到测试类外部则不需要):
class JUnitSampleTests {
static class MyExtension implements TestTemplateInvocationContextProvider {
// 是否支持该模板方法,返回false将不会执行provideTestTemplateInvocationContexts
// (ExtensionContext context)方法,你当然可以根据上下文做些额外判断
@Override
public boolean supportsTestTemplate(ExtensionContext context) {
return true;
}
@Override
public Stream<TestTemplateInvocationContext>
provideTestTemplateInvocationContexts(ExtensionContext context) {
return IntStream.rangeClosed(1, 50) // 将会产生50次参数,意味着模板方法会被执行50次
.mapToObj(n -> new TestTemplateInvocationContext() { // 实例化
@Override
public List<Extension> getAdditionalExtensions() {
return Collections.singletonList(new ParameterResolver() { // 实例化
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext)
throws ParameterResolutionException {
// 判断模板方法的参数类型是否是Integer,因为resolveParameter方法将返回Integer
return parameterContext.getParameter().getType()
.isAssignableFrom(Integer.class);
}
@Override // 产生随机整数
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext)
throws ParameterResolutionException {
return (int) (Math.random() * 100);
}
});
}
});
}
}
}
因为我们是对方法的参数做处理,因此getAdditionalExtensions()
方法中返回的是ParameterResolver
类型,接下来直接将实现类MyExtension
写到@ExtendWith
注解中即可:
class JunitSampleTests {
@TestTemplate
@ExtendWith(MyExtension.class)
void test(Integer val1, Integer val2) {
System.err.println("value 1: " + val1 + " || value 2: " + val2);
}
static class MyExtension implements TestTemplateInvocationContextProvider {
// ...
}
}
方法如期执行了50次,并且每个参数都拿到了值:
上面我特意给了模板方法两个Integer
类型的参数,事实上只要是Integer
类型,不管多少个参数都可以,相对于参数化来说要简便许多。
但这简便是简便了,可只支持Integer
也太过单调了,现实往往没有这样简单。事实上我们只要根据ParameterResolver
方法中提供的参数上下文做进一步判断,从而决定返回的值即可:
new ParameterResolver() {
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext)
throws ParameterResolutionException {
Class<?> type = parameterContext.getParameter().getType();
// 支持Integer和String类型的参数
return type.isAssignableFrom(Integer.class)
|| type.isAssignableFrom(String.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext)
throws ParameterResolutionException {
Class<?> type = parameterContext.getParameter().getType();
if (type.isAssignableFrom(String.class)) {
return UUID.randomUUID().toString();
} else if (type.isAssignableFrom(Integer.class)) {
return (int) (Math.random() * 100);
}
return null;
}
}
模板方法加上一个String
类型参数:
@TestTemplate
@Extensions({ @ExtendWith(MyExtension.class) })
void test(Integer val1, Integer val2, String val3) {
System.err.println("value 1: " + val1 + " || value 2: " + val2 + " || value 3: " + val3);
}
结果完全符合预期:
这个@ExtendWith(MyExtension.class)
注解你当然可以把它标注在测试类上,而不仅仅是模板方法上。对于上面的示例,我们选择直接实现ParameterResolver
接口,实际上你可以用org.junit.jupiter.api.extension.support.TypeBasedParameterResolver
这个抽象类替代,它自身对这个接口做了一些必要实现,可帮助我们减少代码。
软件版本
软件 | 版本 |
---|---|
JUnit | 5.6.2(junit-jupiter) |
结语
我个人认为JUnit
的参数化测试与模板测试是有一定关联的,理解了其中一种,都会为你在学习另一种时提供参考,如有兴趣可去看看我的参数化测试相关文章。