如何在JUnit 5中替换规则

最近发布的JUnit 5(又名JUnit Lambda) Alpha版本引起了我的兴趣,在浏览文档时,我注意到规则以及跑步者和阶级规则都消失了。 根据文档,这些部分竞争的概念已被单个一致的扩展模型取代。

多年来, Frank和我写了一些规则来帮助执行重复性任务,例如测试SWT UI忽略某些环境中的测试注册(测试)OSGi服务在单独的线程中运行测试等等。

因此,我对将现有规则转换为新概念以使它们可以在JUnit 5上本地运行特别感兴趣。为了探索扩展的功能,我选择了两个特性完全不同的规则,并尝试将它们迁移到JUnit 5 。

这些实验的重点是查看规则和扩展之间的概念已发生了变化。 因此,我选择重写JUnit 4意味着不考虑向后兼容性。

如果您有兴趣从JUnit 4迁移到5或探索在JUnit 5中运行现有规则的可能性,则可能需要参加相应的讨论。

第一个候选对象是ConditionalIgnoreRule ,它与@ConditionalIgnore批注一起使用。 该规则评估需要用注释指定的条件,并据此确定是否执行测试。

另一个候选者是内置的TemporaryFolder规则 。 顾名思义,它允许创建在测试完成时删除的文件和文件夹。

因此,它在测试执行之前和之后挂接,以创建一个根目录以在其中存储文件和文件夹并清理该目录。 此外,它提供了实用程序方法来在根目录中创建文件和文件夹。

扩展说明

在详细介绍向扩展的迁移规则之前,让我们简要了解一下新概念。

junit5扩展点类型层次结构

测试执行遵循一定的生命周期。 可以延长生命周期的每个阶段都由一个接口表示。 扩展可以在某些阶段表达兴趣,因为它们实现了相应的接口。

使用ExtendWith批注,测试方法或类可以表示它在运行时需要特定的扩展。 所有扩展都有一个公共的超级接口: ExtensionPointExtensionPoint的类型层次结构列出了扩展当前可以挂接到的所有位置。

例如,下面的代码应用了一个虚构的MockitoExtension来注入模拟对象:

@ExtendWith(MockitoExtension.class)
class MockTest {
  @Mock
  Foo fooMock; // initialized by extension with mock( Foo.class )
}

MockitoExtension将提供一个默认的构造函数,以便可以在运行时实例化它,并实现必要的扩展接口,以便能够将@Mock注入到所有@Mock注释的字段中。

条件忽略

规则的重复模式是提供带有注释的串联服务,该注释用于标记和/或配置希望使用该服务的测试方法。 在这里,ConditionalIgnoreRule检查其运行的所有测试方法,并寻找ConditinalIgnore批注。 如果找到了这样的注释,则评估其条件,如果满足,则忽略测试。

这是ConditionalIgnoreRule实际运行的样子:

@Rule
public ConditionalIgnoreRule rule = new ConditionalIgnoreRule();
 
@Test
@ConditionalIgnore( condition = IsWindowsPlatform.class )
public void testSomethingPlatformSpecific() {
  // ...
}

现在,让我们看一下代码在JUnit 5中的外观:

@Test
@DisabledWhen( IsWindowsPlatform.class )
void testSomethingPlatformSpecific() {
  // ...
}

首先,您会注意到注释已更改其名称。 为了匹配使用术语Disabled而不是被忽略的JUnit 5约定,该扩展还将其名称更改为DisabledWhen

尽管DisabledWhen注释是由DisabledWhenExtension驱动的,但是没有任何东西表明需要扩展。 其原因被称为元注释,并且在查看DisabledWhen的声明方式时可以最好地说明它们:

@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(DisabledWhenExtension.class)
public @interface DisabledWhen {
  Class<? extends DisabledWhenCondition> value();
}

注释(元)带有处理它的扩展名。 并且在运行时,JUnit 5测试执行器负责其余的工作。 如果遇到带注释的测试方法,并且该注释又由ExtendWith元注释,则实例化各个扩展并将其包含在生命周期中。

真的很整洁吗? 在不指定相应规则的情况下注释测试方法时,此技巧还可以避免疏忽。

在幕后, DisabledWhenExtension实现了TestExexutionCondition接口。 对于每个测试方法,都将调用其唯一的evaluate()方法,并且必须返回一个ConditionEvaluationResult ,该ConditionEvaluationResult确定是否应该执行测试。

其余代码与以前基本相同。 查找并发现DisabledWhen批注时,将创建指定条件类的实例,并询问是否应执行测试。 如果执行被拒绝,则返回一个禁用的ConditionEvaluationResult ,并且框架将采取相应的措施。

临时文件夹

在将TemporaryFolder规则变为异常之前,让我们看一下该规则的组成。 首先,该规则将在测试设置和拆卸期间设置并清理一个临时文件夹。 但是,它还为测试提供了访问在该根文件夹内创建(临时)文件和文件夹的方法的权限。

迁移到扩展后,不同的职责变得更加明显。 以下示例显示了如何使用它:

@ExtendWith(TemporaryFolderExtension.class)
class InputOutputTest
  private TemporaryFolder tempFolder;

  @Test
  void testThatUsesTemporaryFolder() {
    F‌ile f‌ile = tempFolder.newF‌ile();
    // ...
  }
}

TemporaryFolderExtension挂接到测试执行生命周期中,以提供和清除临时文件夹,并为所有TemporaryFolder字段提供此类实例。 而TemporaryFolder允许访问在根文件夹中创建文件和文件夹的方法。

为了注入TemporaryFolder ,该扩展实现了InstancePostProcessor接口。 创建测试实例后立即调用其postProcessTestInstance方法。 在该方法中,它可以通过TestExtensionContext参数访问测试实例,并且可以将TemporaryFolder注入所有匹配的字段中。

对于一个类声明多个TemporaryFolder字段的极少数事件,每个字段都被分配一个新实例,并且每个实例都有其自己的根文件夹。

在此过程中创建的所有注入的TemporaryFolder实例都保存在一个集合中,以便稍后进行清理时可以对其进行访问。

要在执行测试后进行清理,需要实现另一个扩展接口: AfterEachExtensionPoint 。 每次测试完成后,将调用其唯一的afterEach方法。 并且此处的TemporaryFolderExtension实现清除所有已知的TemporaryFolder实例。

现在我们可以与TemporaryFolder规则的功能相提并论,现在还可以支持新功能:方法级依赖注入。
在JUnit 5中,现在允许方法具有参数。
这意味着我们的扩展程序不仅应该能够注入字段,而且还应该能够注入TemporaryFolder类型的方法参数。 希望创建临时文件的测试可以请求注入TemporaryFolder如以下示例所示:

class InputOutputTest {
  @Test
  @ExtendWith(TemporaryFolderExtension.class)
  void testThatUsesTemporaryFolder( TemporaryFolder tempFolder ) {
    F‌ile f‌ile = tempFolder.newF‌ile();
    // ...
  }
}

通过实现MethodParameterResolver接口,扩展可以参与解析方法参数。 对于测试方法的每个参数,都会调用扩展的supports()方法来确定它是否可以为给定参数提供值。 对于TemporaryFolderExtension ,实现将检查参数类型是否为TemporaryFolder并在这种情况下返回true 。 如果需要更广泛的上下文,则当前方法调用上下文和扩展上下文还提供了supports()方法。

现在,该扩展程序决定支持某个参数,它的resolve()方法必须提供一个匹配的实例。 同样,提供了周围的环境。 TemporaryFolderExtension只是返回一个唯一的TemporaryFolder实例,该实例知道(临时)根文件夹并提供在其中创建文件和子文件夹的方法。

但是请注意,声明无法解析的参数被视为错误。 因此,如果遇到没有匹配解析器的参数,则会引发异常。

在扩展中存储状态

您可能已经注意到, TemporaryFolderExtension保持其状态(即,它已创建的临时文件夹的列表),当前是一个简单字段。 尽管测试表明这确实可行,但是文档中没有地方指出在调用不同扩展名时都使用同一实例。 因此,如果JUnit 5此时更改其行为,则在这些调用期间状态可能会丢失。

好消息是,JUnit 5提供了一种维护称为Store的扩展状态的方法。 如文档所述,它们为扩展提供了保存和检索数据的方法

该API与简化Map相似,并且允许存储键值对,获取与给定键关联的值以及删除给定键。 键和值都可以是任意对象。 可以通过将TestExtensionContext作为参数传递给每个扩展方法(例如, beforeEachafterEach )来到达存储。每个TestExtensionContext实例都封装了正在执行当前测试的上下文

例如,在beforeEach ,值将存储在扩展上下文中,如下所示:

@Override
public void beforeEach( TestExtensionContext context ) {
  context.getStore().put( KEY, ... );
}

以后可以像这样检索:

@Override
public void afterEach( TestExtensionContext context ) {
  Store store = context.getStore();
  Object value = store.get( KEY );
  // use value...
}

为了避免可能发生的名称冲突,可以为某些命名空间创建存储。 上面使用的context.getStore()方法获取默认名称空间的存储。 要获取特定命名空间的存储,请使用

context.getStore( Namespace.of( MY, NAME, SPACE );

名称空间是通过对象数组{ MY, NAME, SPACE }来定义的。

返还TemporaryFolderExtension以使用Store的练习留给读者。

运行代码

该项目设置为在安装了Maven支持的Eclipse中使用。 但是在具有Maven支持的其他IDE中编译和运行代码并不难。

很自然,在这种早期状态下,尚不支持直接在Eclipse中运行JUnit 5测试。 因此,要运行所有测试,可能需要使用“使用ConsoleRunner运行所有测试”启动配置。 如果遇到麻烦,请参考我以前关于JUnit 5的文章中的“ 使用JUnit 5运行测试”部分,以获得更多提示或发表评论。

总结如何在JUnit 5中替换规则

在这个小小的实验过程中,我给人的印象是,扩展是JUnit 4中规则和朋友的完美替代品。最后,使用新方法很有趣,并且比现有功能更简洁。

如果您发现用扩展尚无法完成的用例,我相信如果让他们知道 JUnit 5团队将不胜感激。

但请注意,在撰写本文时,扩展程序正在进行中 。 该API被标记为实验性的,如有更改,恕不另行通知。 因此,现在实际迁移JUnit 4帮助程序可能还为时过早-除非您不介意将代码调整为可能更改的API。

如果JUnit 5扩展引起了您的兴趣,您可能还需要继续阅读文档相应章节

翻译自: https://www.javacodegeeks.com/2016/04/replace-rules-junit-5.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值