JUnit 5 –条件

最近,我们了解了JUnit的新扩展模型以及它如何使我们能够将自定义行为注入测试引擎。 我向你保证要看情况。 现在就开始吧!

条件允许我们在应该执行或不应该执行测试时定义灵活的标准。 它们的正式名称是“ 条件测试执行”

总览

本系列中有关JUnit 5的其他文章:

在新兴的《 JUnit 5用户指南》中可以找到您将在此处阅读的更多内容以及更多内容。 请注意,它基于Alpha版本,因此可能会发生变化。

确实,我们鼓励我们提出问题或提出请求,以便JUnit 5可以进一步改进。 请利用这个机会! 这是我们帮助JUnit帮助我们的机会,因此,如果您能在这里看到一些改善,请确保将其上游

如有必要,此帖子将得到更新。 我在这里显示的代码示例可以在GitHub上找到

条件扩展点

还记得我们所说的扩展点吗? 没有? 简而言之:它们很多,每个都与特定的接口有关。 这些接口的实现可以传递给JUnit(带有@ExtendWith批注),它将在适当的时候调用它们。

对于条件,需要关注两个扩展点: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
	 */
	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
	 */
	ConditionEvaluationResult evaluate(TestExtensionContext context);

}

ContainerExecutionCondition确定是否执行容器中的测试。 在带有注释测试方法的通常情况下,测试类将是容器。 在同一场景中,单个测试方法的执行由TestExecutionConditions确定。

(我说“在通常情况下”是因为不同的测试引擎对容器和测试的解释可能非常不同。类和方法只是最常见的解释。)

这已经差不多了。 任何条件都应实现这些接口中的一个或两个,并在其评估实现中进行所需的检查。

@已停用

最简单的条件是甚至没有评估的条件:如果存在我们手工制作的注释,我们只是总是禁用测试。

因此,让我们创建@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(
			AnnotatedElement element) {
		Optional<Disabled> disabled = AnnotationUtils
				.findAnnotation(element, Disabled.class);
 
		if (disabled.isPresent())
			return ConditionEvaluationResult
					.disabled(element + " is @Disabled");
 
		return ENABLED;
	}
 
}

很容易做,对吧? 也是正确的,因为它与真正的@Disabled实现几乎相同。 只有两个小区别:

  • 官方注释不需要随身携带扩展名,因为它是默认注册的。
  • 可以给出一个原因,当跳过禁用的测试时会记录该原因。

小警告(当然有一个,您的想法是什么?):AnnotationUtils是内部API,但其功能可能很快就会正式可用

现在,让我们尝试一些不那么琐碎的事情。

@DisabledOnOs

如果我们使用的是正确的操作系统,也许我们只想运行一些测试。

简单的解决方案

同样,我们从注释开始:

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(OsCondition.class)
public @interface DisabledOnOs {
 
	OS[] value() default {};
 
}

这次需要一个值,如果不是,则取一堆,即不应在其上运行测试的操作系统。 OS只是一个枚举,每个操作系统都有一个值。 而且它有一个方便的静态OS define()方法,您猜对了,它确定了代码在其上运行的操作系统。

这样,让我们​​转向OsCondition。 它必须检查注释是否存在,但还要检查当前的操作系统是否是赋予注释的操作系统之一。

public class OsCondition 
		implements ContainerExecutionCondition, TestExecutionCondition {
 
	// both `evaluate` methods forward to `evaluateIfAnnotated` as above
 
	private ConditionEvaluationResult evaluateIfAnnotated(
			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);
}

真好

少礼

但是我们可以做得更好! 借助JUnit的可自定义注释,我们可以使此条件更加平滑:

@TestExceptOnOs(OS.WINDOWS)
void doesNotRunOnWindowsEither() {
	assertTrue(false);
}

要实现@TestExceptOnOs,只需执行以下操作就可以了:

@Retention(RetentionPolicy.RUNTIME)
@Test
@DisabledOnOs(/* somehow get the `value` below */)
public @interface TestExceptOnOs {
 
	OS[] value() default {};
 
}

在执行测试并扫描OsCondition :: evaluateIfAnnotated中的@DisabledOnOs时,我们会发现它在@TestExceptOnOs上进行了元注释,并且我们的逻辑将正常工作。 但是我找不到让@DisabledOnOs访问给@TestExceptOnOs的OS值的方法。 :( (你可以吗?)

下一个最佳选择是对新注释简单地使用相同的扩展名:

@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(OsCondition.class)
@Test
public @interface TestExceptOnOs {

	OS[] value() default {};

}

然后我们拉皮条OsCondition :: evaluateIfAnnotated包括新的情况…

private ConditionEvaluationResult evaluateIfAnnotated(
		AnnotatedElement element) {
	Optional<DisabledOnOs> disabled = AnnotationUtils
		.findAnnotation(element, DisabledOnOs.class);
	if (disabled.isPresent())
		return disabledIfOn(disabled.get().value());
 
	Optional<TestExceptOnOs> testExcept = AnnotationUtils
		.findAnnotation(element, TestExceptOnOs.class);
	if (testExcept.isPresent())
		return disabledIfOn(testExcept.get().value());
 
	return ConditionEvaluationResult.enabled("");
}

……我们完成了。 现在我们确实可以按照我们希望的方式使用它。

抛光

创建倒置的注释(如果不在指定的操作系统之一上禁用,则完全相同),但是有了它们,改进的名称和静态导入,我们可以在这里结束:

@TestOn(WINDOWS)
void doesNotRunOnWindowsEither() {
	assertTrue(false);
}

还不错吧?

junit-5-条件

在CC-BY 2.0下由CWCS托管主机发布

@DisabledIfTestFails

让我们再尝试一件事-这次我们将使其变得非常有趣! 假设有很多(集成?)测试,并且如果其中一个测试由于特定的异常而失败,那么其他测试也必然会失败。 因此,为了节省时间,我们想禁用它们。

那么,我们在这里需要什么? 显而易见,我们必须以某种方式收集在测试执行过程中引发的异常。 这必须与测试类的生存期绑定,因此我们不会禁用测试,因为某些异常会在完全不同的测试类中发生。 然后,我们需要一个条件实现,该条件实现检查是否抛出了特定的异常,如果存在则禁用测试。

收集例外

查看扩展点列表,我们发现“异常处理”。 相应的接口看起来很有希望:

/**
 * ExceptionHandlerExtensionPoint defines the API for Extension Extensions
 * that wish to react to thrown exceptions in tests.
 *
 * [...]
 */
public interface ExceptionHandlerExtensionPoint extends ExtensionPoint {

	/**
	 * React to a throwable which has been thrown by a test method.
	 *
	 * Implementors have to decide if they
	 * 
	 * - Rethrow the incoming throwable
	 * - Throw a newly constructed Exception or Throwable
	 * - Swallow the incoming throwable
	 *
	 * [...]
	 */
	void handleException(TestExtensionContext context, Throwable throwable)
			throws Throwable;
}

因此,我们将实现handleException来存储然后重新抛出异常。

您可能还记得我写的有关扩展和状态的内容:

引擎在实例化扩展以及将实例保留多长时间时不做任何保证,因此它们必须是无状态的。 他们需要维护的任何状态都必须写入JUnit并从中加载。

好的,所以我们使用商店。 有效地收集了我们想记住的东西。 我们可以通过传递给大多数扩展方法的扩展上下文来访问它。 稍作修改后发现,每个上下文都有其自己的存储,因此我们必须决定要访问哪个上下文。

每个测试方法(TestExtensionContext)和整个测试类(ContainerExtensionContext)都有一个上下文。 请记住,我们想将在执行所有测试期间抛出的所有异常存储在一个类中,但不能存储更多,即,不是其他测试类抛出的异常。 事实证明,ContainerExtensionContext及其存储正是我们需要的。

因此,这里我们获取容器上下文并使用它来存储一组引发的异常:

private static final Namespace NAMESPACE = Namespace
		.of("org", "codefx", "CollectExceptions");
private static final String THROWN_EXCEPTIONS_KEY = "THROWN_EXCEPTIONS_KEY";
 
@SuppressWarnings("unchecked")
private static Set<Exception> getThrown(ExtensionContext context) {
	ExtensionContext containerContext = getAncestorContainerContext(context)
			.orElseThrow(IllegalStateException::new);
	return (Set<Exception>) containerContext
			.getStore(NAMESPACE)
			.getOrComputeIfAbsent(
					THROWN_EXCEPTIONS_KEY,
					ignoredKey -> new HashSet<>());
}
 
private static Optional<ExtensionContext> getAncestorContainerContext(
		ExtensionContext context) {
	Optional<ExtensionContext> containerContext = Optional.of(context);
	while (containerContext.isPresent()
			&& !(containerContext.get() instanceof ContainerExtensionContext))
		containerContext = containerContext.get().getParent();
	return containerContext;
}

现在添加一个异常很简单:

@Override
public void handleException(TestExtensionContext context, Throwable throwable)
		throws Throwable {
	if (throwable instanceof Exception)
		getThrown(context).add((Exception) throwable);
	throw throwable;
}

实际上,这本身就是一个有趣的扩展。 也许它也可以用于分析。 无论如何,我们将要查看抛出的异常,因此我们需要一个公共方法:

public static Stream<Exception> getThrownExceptions(
		ExtensionContext context) {
	return getThrown(context).stream();
}

使用此扩展,任何其他扩展都可以检查到目前为止已抛出了哪些异常。

禁用

其余部分与以前非常相似,因此让我们快速了解一下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(DisabledIfTestFailedCondition.class)
public @interface DisabledIfTestFailedWith {
 
	Class<? extends Exception>[] value() default {};
 
}

请注意,我们仅在方法上允许使用此注释。 在测试类上使用它可能很有意义,但现在让我们保持简单。 因此,我们仅实现TestExecutionCondition。 在检查我们的注释是否存在之后,我们使用用户提供的异常类调用disableIfExceptionWasThrown:

private ConditionEvaluationResult disableIfExceptionWasThrown(
		TestExtensionContext context,
		Class<? extends Exception>[] exceptions) {
	return Arrays.stream(exceptions)
			.filter(ex -> wasThrown(context, ex))
			.findAny()
			.map(thrown -> ConditionEvaluationResult.disabled(
					thrown.getSimpleName() + " was thrown."))
			.orElseGet(() -> ConditionEvaluationResult.enabled(""));
}
 
private static boolean wasThrown(
		TestExtensionContext context, Class<? extends Exception> exception) {
	return CollectExceptionExtension.getThrownExceptions(context)
			.map(Object::getClass)
			.anyMatch(exception::isAssignableFrom);
}
把它放在一起

如果在此之前抛出了特定类型的异常,这就是我们使用这些注释来禁用测试的方式:

@CollectExceptions
class DisabledIfFailsTest {
 
	private static boolean failedFirst = false;
 
	@Test
	void throwException() {
		System.out.println("I failed!");
		failedFirst = true;
		throw new RuntimeException();
	}
 
	@Test
	@DisabledIfTestFailedWith(RuntimeException.class)
	void disableIfOtherFailedFirst() {
		System.out.println("Nobody failed yet! (Right?)");
		assertFalse(failedFirst);
	}
 
}

摘要

哇,那是很多代码! 但是到目前为止,我们真的知道如何在JUnit 5中实现条件:

  • 创建所需的注释和@ExtendWith条件实现
  • 实现ContainerExecutionCondition,TestExecutionCondition或同时实现
  • 检查是否存在新的注释
  • 进行实际检查并返回结果

我们还看到,这可以与其他扩展点结合使用,如何使用商店来保留信息,并且自定义注释可以使扩展使用起来更加优雅。

有关标记扩展点的更多乐趣,请在讨论参数注入时查看本系列的下一篇文章。

翻译自: https://www.javacodegeeks.com/2016/05/junit-5-conditions.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值