测试方案模板_JUnit 5模板测试

23d11faae431107c17d805e436db7bef.png

前言

之前我写过两篇关于JUnit5参数化测试的文章,而今将要提到的模板测试其实跟参数化测试也有异曲同工之处。在JUnit5中,如果你使用了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,不能被privatestatic修饰:

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

有必要看看其继承树,方便在使用时参考:

555326d362fec79774faa8d93770a662.png

@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次,并且每个参数都拿到了值:

25858d07eab651b1e4822a19ee97181c.png

上面我特意给了模板方法两个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);
    }

结果完全符合预期:

dd7f6188a8d3850dfae30c7dfd3dce0f.png

这个@ExtendWith(MyExtension.class)注解你当然可以把它标注在测试类上,而不仅仅是模板方法上。对于上面的示例,我们选择直接实现ParameterResolver接口,实际上你可以用org.junit.jupiter.api.extension.support.TypeBasedParameterResolver这个抽象类替代,它自身对这个接口做了一些必要实现,可帮助我们减少代码。

软件版本

软件版本
JUnit5.6.2(junit-jupiter)

结语

我个人认为JUnit的参数化测试与模板测试是有一定关联的,理解了其中一种,都会为你在学习另一种时提供参考,如有兴趣可去看看我的参数化测试相关文章。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值