扩展 junit 框架
JUnit是JVM上最受欢迎的测试框架,在其第5个主要发行版中已进行了全面改进。 JUnit 5包含了丰富的功能-从改进的注释,标记和过滤到条件测试执行和声明消息的惰性评估。 这使得本着TDD精神编写单元测试变得轻而易举。 新框架还引入了一个单一但功能强大的扩展模型。 扩展开发人员可以使用此新模型向JUnit 5添加自定义功能。本文将引导您完成自定义扩展的设计和实现。 此自定义扩展为Java程序员提供了一种创建和执行故事和行为(即BDD规范测试)的方式。
我们首先探讨使用JUnit 5和我们的自定义扩展(方便地称为“ StoryExtension”)编写的示例故事和行为(测试方法)。 该示例使用了两个新的自定义注释“ @Story”和“ @Scenario”,以及一个我们将要设计的新“ Scene”类,以支持我们的自定义StoryExtension:
import org.junit.jupiter.api.extension.ExtendWith;
import ud.junit.bdd.ext.Scenario;
import ud.junit.bdd.ext.Scene;
import ud.junit.bdd.ext.Story;
import ud.junit.bdd.ext.StoryExtension;
@ExtendWith(StoryExtension.class)
@Story(name=“Returns go back to the stockpile”, description=“...“)
public class StoreFrontTest {
@Scenario(“Refunded items should be returned to the stockpile”)
public void refundedItemsShouldBeRestocked(Scene scene) {
scene
.given(“customer bought a blue sweater”,
() -> buySweater(scene, “blue”))
.and(“I have three blue sweaters in stock”,
() -> assertEquals(3, sweaterCount(scene, “blue”),
“Store should carry 3 blue sweaters”))
.when(“the customer returns the blue sweater for a refund”,
() -> refund(scene, 1, “blue”))
.then(“I should have four blue sweaters in stock”,
() -> assertEquals(4, sweaterCount(scene, “blue”),
“Store should carry 4 blue sweaters”))
.run();
}
}
另外,当使用我们的自定义扩展名执行测试时,将生成如下所示的文本报告:
STORY: Returns go back to the stockpile
As a store owner, in order to keep track of stock, I want to add items back to stock when they’re returned.
SCENARIO: Refunded items should be returned to stock
GIVEN that a customer previously bought a blue sweater from me
AND I have three blue sweaters in stock
WHEN the customer returns the blue sweater for a refund
THEN I should have four blue sweaters in stock
这些报告可以用作应用程序功能集的实时文档。
自定义扩展StoryExtension能够借助以下核心概念来支持和执行故事和行为:
先前在示例故事中看到的“ @ExtendWith”注释是Jupiter提供的标记界面。 这是在测试类或方法上注册自定义扩展的一种声明方式。 它告诉Jupiter的测试引擎为给定的类或方法调用自定义扩展。 或者,自定义扩展可以由测试编写者以编程方式注册,也可以使用服务加载程序机制自动(全局)注册。
我们的自定义扩展程序需要一种识别故事的方法。 为此,我们定义了一个名为“ Story”的自定义注释类,如下所示:
import org.junit.platform.commons.annotation.Testable;
@Testable
public @interface Story {...}
测试作者应使用此自定义批注将测试类标记为故事。 请注意,该注释使用JUnit 5的内置“ @Testable”注释进行元注释。 该注释为IDE和其他工具提供了一种方法,该方法可以标识可测试的类和方法-意味着带注释的类或方法可以由JUnit 5 Jupiter测试引擎之类的测试引擎执行。
我们的自定义扩展程序还需要一种方法来识别故事中的行为或场景。 为此,我们定义了一个自定义注释类,您猜对了,它是“ Scenario”,它看起来像这样:
import org.junit.jupiter.api.Test;
@Test
public @interface Scenario {...}
测试作者应使用此自定义批注将测试方法标记为方案。 注释使用JUnit 5 Jupiter的内置“ @Test”注释进行元注释。 当IDE和测试引擎扫描给定的一组测试类并在公共实例方法上找到此自定义@Scenario批注时,它们会将这些方法标记为要执行的测试方法。
请注意,与JUnit 4 @Test注释不同,Jupiter的@Test注释不支持可选的“期望”异常和“超时”参数。 与JUnit 4 @Test注释不同,Jupiter的@Test注释是从头开始设计的,并考虑了自定义扩展。
JUnit 5 Jupiter提供了扩展作者回调,可用于利用测试的生命周期事件。 扩展模型提供了几个接口,用于在测试执行生命周期的各个点扩展测试:
扩展作者可以自由实现所有或某些生命周期接口。
“ BeforeAllCallback”接口提供了一种在调用JUnit测试容器中的测试之前初始化扩展并添加自定义逻辑的方法。 我们的StoryExtension类将实现此接口,以确保给定的测试类使用“ @Story”注释进行修饰。
import org.junit.jupiter.api.extension.BeforeAllCallback;
public class StoryExtension implements BeforeAllCallback {
@Override
public void beforeAll(ExtensionContext context) throws Exception {
if (!AnnotationSupport
.isAnnotated(context.getRequiredTestClass(), Story.class)) {
throw new Exception(“Use @Story annotation...“);
}
}
}
Jupiter引擎将提供一个扩展将在其下运行的执行上下文实例。 我们使用此上下文来确定正在执行的测试类是否已使用所需的“ @Story”注释进行修饰。 我们利用JUnit平台提供的AnnotationSupport帮助器类来检查是否存在注释。
回想一下,我们的自定义扩展程序在执行测试后会生成BDD报告。 这些报告的某些部分是从“ @Store”注释的元素中提取的。 我们使用beforeAll回调存储这些字符串。 稍后,在执行生命周期结束时,我们检索这些字符串以生成报告。 为此使用了一个简单的POJO。 我们将此类命名为“ StoryDetails”。 以下代码段演示了创建此类实例并将注释元素保存到实例中的过程:
public class StoryExtension implements BeforeAllCallback {
@Override
public void beforeAll(ExtensionContext context) throws Exception {
Class> clazz = context.getRequiredTestClass();
Story story = clazz.getAnnotation(Story.class);
StoryDetails storyDetails = new StoryDetails()
.setName(story.name())
.setDescription(story.description())
.setClassName(clazz.getName());
context.getStore(NAMESPACE).put(clazz.getName(), storyDetails);
}
}
上面方法中的最后一个陈述值得更详细的解释。 我们实质上是从执行上下文中检索命名存储,并将新创建的“ StoryDetails”实例推入该存储中。
商店是持有人,自定义扩展可以使用它来保存和检索任意数据-基本上是一个收费的内存映射。 为了避免多个扩展之间的意外键冲突,JUnit的好伙伴引入了名称空间的概念。 名称空间是一种范围扩展功能所保存的数据的方式。 唯一作用域扩展数据的一种常用方法是使用自定义扩展类名称:
private static final Namespace NAMESPACE = Namespace
.create(StoryExtension.class);
我们的扩展程序需要的另一个自定义注释是“ @Scenario”注释。 此注释用于将测试方法标记为描述故事中的场景或行为。 我们的扩展程序将解析这些场景,以便将它们作为JUnit测试执行并生成报告。 从我们之前看到的生命周期图中回顾“ BeforeEachCallback”接口; 我们将在调用每个测试方法之前使用回调添加此附加逻辑:
import org.junit.jupiter.api.extension.BeforeEachCallback;
public class StoryExtension implements BeforeEachCallback {
@Override
public void beforeEach(ExtensionContext context) throws Exception {
if (!AnnotationSupport.
isAnnotated(context.getRequiredTestMethod(), Scenario.class)) {
throw new Exception(“Use @Scenario annotation...“);
}
}
}
如前所述,Jupiter引擎将提供扩展将在其下运行的执行上下文实例。 我们使用上下文来确定执行中的测试方法是否用必需的“ @Scenario”注释修饰。
回到本文开头,我们在代码中描述了一个示例故事,我们的自定义扩展负责将“ Scene”类的实例注入每种测试方法。 Scene类使测试编写者可以使用写为lambda表达式的“给定”,“那么”和“何时”等步骤定义场景(行为)。 Scene类是我们自定义扩展的中心单元,其中包含测试方法特定的状态信息。 状态信息可以在方案的各个步骤之间传递。 我们使用“ BeforeEachCallback”接口在调用测试方法之前准备一个Scene实例:如前所述,Jupiter引擎将提供一个执行上下文实例,扩展将在该实例下运行。 我们使用上下文来确定执行中的测试方法是否用必需的“ @Scenario”注释修饰。
public class StoryExtension implements BeforeEachCallback {
@Override
public void beforeEach(ExtensionContext context) throws Exception {
Scene scene = new Scene()
.setDescription(getValue(context, Scenario.class));
Class> clazz = context.getRequiredTestClass();
StoryDetails details = context.getStore(NAMESPACE)
.get(clazz.getName(), StoryDetails.class);
details.put(scene.getMethodName(), scene);
}
}
上面的代码与我们在“ BeforeAllCallback”接口方法中所做的非常相似。
此时缺少的难题是能够将独特的场景实例注入测试方法中。 Jupiter的扩展模型为此目的提供了一个接口。 它称为“ ParameterResolver”接口。 该接口为测试引擎提供了一种方法,该方法可以识别希望在测试执行期间动态注入参数的扩展。 我们需要实现此接口提供的两种方法,以注入场景实例:
import org.junit.jupiter.api.extension.ParameterResolver;
public class StoryExtension implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) {
Parameter parameter = parameterContext.getParameter();
return Scene.class.equals(parameter.getType());
}
@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) {
Class> clazz = extensionContext.getRequiredTestClass();
StoryDetails details = extensionContext.getStore(NAMESPACE)
.get(clazz.getName(), StoryDetails.class);
return details.get(extensionContext
.getRequiredTestMethod().getName());
}
}
在第二种方法“ resolveParameter()”中,我们从执行上下文的名称空间作用域存储中检索StoryDetails实例。 从那里,我们为给定的测试方法检索先前创建的场景实例,并将其传递给测试引擎。 测试引擎会将场景实例注入到测试方法中并执行测试。 请注意,仅当“ supportsParameter()”方法返回真值时,才调用“ resolveParameter()”方法。
最后,为了在执行所有故事和场景之后生成报告,自定义扩展钩接到“ AfterAllCallback”接口中:
import org.junit.jupiter.api.extension.AfterAllCallback;
public class StoryExtension implements AfterAllCallback {
@Override
public void afterAll(ExtensionContext context) throws Exception {
new StoryWriter(getStoryDetails(context)).write();
}
}
“ StoryWriter”是一个自定义类,可生成报告并将其保存到JSON或文本文件中。
所有关键部分都准备就绪后,让我们看看如何使用此自定义扩展使用gradle编写BDD样式测试。 Gradle 4.6及更高版本支持使用JUnit 5运行单元测试。您可以使用build.gradle文件配置JUnit 5。
dependencies {
testCompile group: “ud.junit.bdd”, name: “bdd-junit”,
version: “0.0.1-SNAPSHOT”
testCompile group: “org.junit.jupiter”, name: “junit-jupiter-api”,
version: “5.2.0"
testRuntime group: “org.junit.jupiter”, name: “junit-jupiter-engine”,
version: “5.2.0”
}
test {
useJUnitPlatform()
}
如您所见,使用“ useJUnitPlatform()”方法,我们明确要求gradle使用JUnit5。然后,我们可以使用StoryExtension类开始编写测试。 这是本文开头的示例:
import org.junit.jupiter.api.extension.ExtendWith;
import ud.junit.bdd.ext.Scenario;
import ud.junit.bdd.ext.Story;
import ud.junit.bdd.ext.StoryExtension;
@ExtendWith(StoryExtension.class)
@Story(name=“Returns go back to the stockpile”, description=“...“)
public class StoreFrontTest {
@Scenario(“Refunded items should be returned to the stockpile”)
public void refundedItemsShouldBeRestocked(Scene scene) {
scene
.given(“customer bought a blue sweater”,
() -> buySweater(scene, “blue”))
.and(“I have three blue sweaters in stock”,
() -> assertEquals(3, sweaterCount(scene, “blue”),
“Store should carry 3 blue sweaters”))
.when(“the customer returns the blue sweater for a refund”,
() -> refund(scene, 1, “blue”))
.then(“I should have four blue sweaters in stock”,
() -> assertEquals(4, sweaterCount(scene, “blue”),
“Store should carry 4 blue sweaters”))
.run();
}
}
我们可以使用“ gradle testClasses”运行测试,也可以使用您喜欢的支持JUnit 5的IDE。自定义扩展与常规测试报告一起,为使用它的所有测试类生成BDD文档。
我们描述了JUnit 5扩展模型以及如何利用它来创建自定义扩展。 在此过程中,我们设计并实现了一个自定义扩展,供测试作者使用以创建和执行故事。 前往GitHub获取代码并研究自定义扩展以及如何使用Jupiter扩展模型及其API实施该扩展。
翻译自: https://www.infoq.com/articles/deep-dive-junit5-extensions/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1
扩展 junit 框架