原文地址:http://blog.codefx.org/libraries/junit-5-conditions/
原文日期:08, May, 2016
译文首发:Linesh 的博客:「译」JUnit 5 系列:条件测试
我的 Github:http://github.com/linesh-simplicity
上一节我们了解了 JUnit 新的扩展模型,了解了它是如何支持我们向引擎定制一些行为的。然后我还预告会为大家讲解条件测试,这一节主题就是它了。
条件测试,指的是允许我们自定义灵活的标准,来决定一个测试是否应该执行。条件(condition) 官方的叫法是条件测试执行。
概述
(如果不喜欢看文章,你可以戳这里看我的演讲,或者看一下最近的 vJUG 讲座,或者我在 DevoxxPL 上的 PPT。
本系列文章都基于 Junit 5发布的先行版 Milestone 2。它可能会有变化。如果有新的里程碑(milestone)版本发布,或者试用版正式发行时,我会再来更新这篇文章。
这里要介绍的多数知识你都可以在 JUnit 5 用户指南 中找到(这个链接指向的是先行版 Milestone 2,想看的最新版本文档的话请戳这里),并且指南还有更多的内容等待你发掘。下面的所有代码都可以在 我的 Github 上找到。
目录
- 相关的扩展点
- 动手实现一个@Disabled 注解
- @DisabledOnOs
- 一种简单的实现方式
- 更简洁的API
- 代码重构
- @DisabledIfTestFails
- 异常收集
- 禁用测试
- 集成
- 回顾总结
- 分享&关注
相关的扩展点
还记得 拓展点 一节讲的内容吗?不记得了?好吧,简单来说,JUnit 5 中定义了许多扩展点,每个扩展点都对应一个接口。你自己的扩展可以实现其中的某些接口,然后通过 @ExtendWith
注解注册给 JUnit,后者会在特定的时间点调用你的接口实现。
要实现条件测试,你需要关注其中的两个扩展点: ContainerExecutionCondition
(容器执行条件)和 TestExecutionCondition
(测试执行条件)。
public interface ContainerExecutionCondition extends Extension {
/**
* Evaluate this condition for the supplied ContainerExtensionContext.
*
* An enabled result indicates that the container should be executed;
* whereas, a disabled result indicates that the container should not
* be executed.
*
* @param context the current ContainerExtensionContext; never null
* @return the result of evaluating this condition; never null
*/
ConditionEvaluationResult evaluate(ContainerExtensionContext context);
}
public interface TestExecutionCondition extends Extension {
/**
* Evaluate this condition for the supplied TestExtensionContext.
*
* An enabled result indicates that the test should be executed;
* whereas, a disabled result indicates that the test should not
* be executed.
*
* @param context the current TestExtensionContext; never null
* @return the result of evaluating this condition; never null
*/
ConditionEvaluationResult evaluate(TestExtensionContext context);
}
ContainerExecutionCondition
接口将决定容器中的测试是否会被执行。通常情况下,你使用 @Test
注解来标记测试,此时测试所在的类就是容器。同时,单独的测试方法是否执行则是由 TestExecutionCondition
接口决定的。
(这里,我说的是“通常情况下”,因为其他测试引擎可能对容器和测试有截然不同的定义。但一般情况下,测试就是单个的方法,容器指的就是测试类。)
嗯,基本知识就这么多。想实现条件测试,至少需要实现以上两个接口中的一个,并在接口的 evalute
方法中执行自己的条件检查。
动手实现一个@Disabled 注解
最简单的“条件”就是判断都没有,直接禁用测试。如果在方法上发现了 @Disabled
注解,我们就直接禁用该测试。
让我们来写一个这样的 @Disabled
注解吧:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(@DisabledCondition.class)
public @interface Disabled { }
对应的扩展如下:
public class DisabledCondition
implements ContainerExecutionCondition, TestExecutionCondition {
private static final ConditionEvaluationResult ENABLED =
ConditionEvaluationResult.enabled("@Disabled is not present");
@Override
public ConditionEvaluationResult evaluate(
ContainerExtensionContext context) {
return evaluateIfAnnotated(context.getElement());
}
@Override
public ConditionEvaluationResult evaluate(
TestExtensionContext context) {
return evaluateIfAnnotated(context.getElement());
}
private ConditionEvaluationResult evaluateIfAnnotated(
Optional<AnnotatedElement> element) {
Optional<Disabled> disabled = AnnotationUtils
.findAnnotation(element, Disabled.class);
if (disabled.isPresent())
return ConditionEvaluationResult
.disabled(element + " is @Disabled");
return ENABLED;
}
}
写起来小菜一碟吧?在 JUnit 真实的产品代码中,@Disabled
也是这么实现的。不过,有两个地方有一些细微的差别:
- 官方
@Disabled
注解不需要再使用@ExtendWith
注册扩展,因为它是默认注册了的 - 官方
@Disabled
注解可以接收一个参数,解释测试被忽略的理由。它会在测试被忽略时被记录下来
使用时请注意,AnnotationUtils
是个内部 API。不过,官方可能很快就会将它提供的功能给开放出来。
接下来让我们写点更有意思的东西吧。
@DisabledOnOs
如果有些测试我们只想让它在特定的操作系统上面运行,这个要怎么实现呢?
一种简单的实现方式
当然,我们还是从注解开始咯:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(OsCondition.class)
public @interface DisabledOnOs {
OS[] value() default {};
}
这回注解需要接收一个或多个参数值,你需要告诉它想禁用测试的操作系统有哪些。 OS
是个枚举类,定义了所有操作系统的名字。同时,它还提供了一个静态的 static OS determine()
方法,你可能已经从名字猜到了,它会推断并返回你当前所用的操作系统。
现在我们可以着手实现 OsCondition
扩展类了。它必须检查两点:注解是否存在,以及当前操作系统是否在注解声明的禁用列表中。
public class OsCondition
implements ContainerExecutionCondition, TestExecutionCondition {
// both `evaluate` methods forward to `evaluateIfAnnotated` as above
private ConditionEvaluationResult evaluateIfAnnotated(
Optional<AnnotatedElement> element) {
Optional<DisabledOnOs> disabled = AnnotationUtils
.findAnnotation(element, DisabledOnOs.class);
if (disabled.isPresent())
return disabledIfOn(disabled.get().value());
return ENABLED;
}
private ConditionEvaluationResult disabledIfOn(OS[] disabledOnOs) {
OS os = OS.determine();
if (Arrays.asList(disabledOnOs).contains(os))
return ConditionEvaluationResult
.disabled("Test is disabled on " + os + ".");
else
return ConditionEvaluationResult
.enabled("Test is not disabled on " + os + ".");
}
}
然后使用的时候就可以像这样:
@Test
@DisabledOnOs(OS.WINDOWS)
void doesNotRunOnWindows() {
assertTrue(false);
}
棒。
更简洁的API
但代码还可以写得更好!JUnit 的注解是可组合的,基于此我们可以让这个条件注解更简洁:
@TestExceptOnOs(OS.WINDOWS)
void doesNotRunOnWindowsEither() {
assertTrue(false);
}
@TestExceptionOnOs
完美的实现方案是这样的:
@Retention(RetentionPolicy.RUNTIME)
@Test
@DisabledOnOs(/* 通过某种方式取得注解下的 `value` 值 */)
public @interface TestExceptOnOs {
OS[] value() default {};
}
测试实际运行时, OsCondition::evaluateIfAnnotated
方法会扫描 @DisabledOnOs
注解,然后我们发现它又是对 @TestExceptOnOs
的注解,前面写的代码就可以如期工作了。但我不知道如何在 @DisabledOnOs
注解中获取 @TestExceptOnOs
中的value()
值。