使用 JUnit 5 進(jìn)行單元測(cè)試
動(dòng)態(tài)測(cè)試
目前所介紹的 JUnit 5 測(cè)試方法的創(chuàng)建都是靜態(tài)的,在編譯時(shí)刻已經(jīng)存在。JUnit 5 新增了對(duì)動(dòng)態(tài)測(cè)試的支持,可以在運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建測(cè)試并執(zhí)行。通過動(dòng)態(tài)測(cè)試,可以滿足一些靜態(tài)測(cè)試無法解的需求,也可以完成一些重復(fù)性很高的測(cè)試。比如,有些測(cè)試用例可能依賴運(yùn)行時(shí)的變量,有時(shí)候會(huì)需要生成上百個(gè)不同的測(cè)試用例。這些場(chǎng)景都是動(dòng)態(tài)測(cè)試可以發(fā)揮其長(zhǎng)處的地方。動(dòng)態(tài)測(cè)試是通過新的@TestFactory 注解來實(shí)現(xiàn)的。測(cè)試類中的方法可以添加@TestFactory 注解的方法來聲明其是創(chuàng)建動(dòng)態(tài)測(cè)試的工廠方法。這樣的工廠方法需要返回 org.junit.jupiter.api.DynamicTest 類的集合,可以是 Stream、Collection、Iterable 或 Iterator 對(duì)象。每個(gè)表示動(dòng)態(tài)測(cè)試的 DynamicTest 對(duì)象由顯示名稱和對(duì)應(yīng)的 Executable 接口的實(shí)現(xiàn)對(duì)象來組成。清單 13 中展示了@TestFactory 的示例。
清單 13. 動(dòng)態(tài)測(cè)試
@TestFactory
public Collection<DynamicTest> simpleDynamicTest() {
return Collections.singleton(dynamicTest
("simple dynamic test", () -> assertTrue(2 > 1)));
}
DynamicTest 提供了一個(gè)靜態(tài)方法 stream 來根據(jù)輸入生成動(dòng)態(tài)測(cè)試,如清單 14 所示。
清單 14. 通過 stream 方法來生成動(dòng)態(tài)測(cè)試
@TestFactory
public Stream<DynamicTest> streamDynamicTest() {
return stream(
Stream.of("Hello", "World").iterator(),
(word) -> String.format("Test - %s", word),
(word) -> assertTrue(word.length() > 4)
);
}
執(zhí)行測(cè)試用例
JUnit 5 提供了三種不同的方式來執(zhí)行測(cè)試用例,分別是通過 Gradle 插件、Maven 插件和命令行來運(yùn)行。
Gradle
JUnit 5 提供了 Gradle 插件,在 Gradle 項(xiàng)目中運(yùn)行單元測(cè)試,如清單 15 所示。
清單 15. 使用 JUnit 5 的 Gradle 插件
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.0-M2'
}
}
apply plugin: 'org.junit.platform.gradle.plugin'
在啟用了 Gradle 插件之后,可以通過 junitPlatformTest 任務(wù)來運(yùn)行單元測(cè)試?梢栽 Gradle 腳本中對(duì)插件進(jìn)行定制,如通過 reportsDir 設(shè)置測(cè)試結(jié)果報(bào)告的生成路徑,通過 tags 來設(shè)置包含或排除的標(biāo)簽名稱,如清單 16 所示。
清單 16. 配置 JUnit 5 的 Gradle 插件
junitPlatform {
platformVersion 1.0
reportsDir "build/test-results/junit-platform"
tags {
include 'fast', 'smoke'
}
}
Maven
在 Maven 項(xiàng)目中可以通過 Surefire 插件來運(yùn)行 JUnit 5 測(cè)試,只需要在 POM 文件中進(jìn)行配置即可。如清單 17 所示。
清單 17. 在 Maven 項(xiàng)目中使用 JUnit 5
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.19</version>
<dependencies>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-surefire-provider</artifactId>
<version>1.0.0-M2</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
命令行
除了 Gradle 和 Maven 之外,還可以通過命令行來運(yùn)行 JUnit 5 測(cè)試。只需要直接運(yùn)行 Java 類 org.junit.platform.console.ConsoleLauncher 即可。ConsoleLauncher 提供了不同的命令行參數(shù)來配置測(cè)試運(yùn)行的行為,如-n 來指定包含的 Java 類名滿足的模式,-t 來包含標(biāo)簽,-T 來排除標(biāo)簽。
擴(kuò)展機(jī)制
JUnit 5 提供了標(biāo)準(zhǔn)的擴(kuò)展機(jī)制來允許開發(fā)人員對(duì) JUnit 5 的功能進(jìn)行增強(qiáng)。JUnit 5 提供了很多的標(biāo)準(zhǔn)擴(kuò)展接口,第三方可以直接實(shí)現(xiàn)這些接口來提供自定義的行為。通過@ExtendWith 注解可以聲明在測(cè)試方法和類的執(zhí)行中啟用相應(yīng)的擴(kuò)展。
擴(kuò)展的啟用是繼承的,這既包括測(cè)試類本身的層次結(jié)構(gòu),也包括測(cè)試類中的測(cè)試方法。也是說,測(cè)試類會(huì)繼承其父類中的擴(kuò)展,測(cè)試方法會(huì)繼承其所在類中的擴(kuò)展。除此之外,在一個(gè)測(cè)試上下文中,每一個(gè)擴(kuò)展只能出現(xiàn)一次。
創(chuàng)建擴(kuò)展
JUnit 5 中的擴(kuò)展非常容易創(chuàng)建,只是實(shí)現(xiàn)了特定接口的 Java 類。JUnit 5 的擴(kuò)展都需要實(shí)現(xiàn) org.junit.jupiter.api.extension.Extension 接口,不過該接口只是一個(gè)標(biāo)記接口,并沒有任何需要實(shí)現(xiàn)的具體方法。真正起作用的是 Extension 的子接口,作為 JUnit 5 提供的擴(kuò)展點(diǎn)。
測(cè)試執(zhí)行條件
ContainerExecutionCondition 和 TestExecutionCondition 接口用來配置是否啟用測(cè)試類或測(cè)試方法。前面提到的@Disabled 注解也是通過這樣的機(jī)制來實(shí)現(xiàn)的。ContainerExecutionCondition 接口對(duì)應(yīng)的是測(cè)試類,而 TestExecutionCondition 接口對(duì)應(yīng)的是測(cè)試方法。
ContainerExecutionCondition 接口的 evaluate 方法接受 ContainerExtensionContext 接口作為參數(shù),并返回 ConditionEvaluationResult 類的對(duì)象作為結(jié)果。通過 ContainerExtensionContext 接口可以獲取到當(dāng)前測(cè)試類的上下文信息,而 ConditionEvaluationResult 類則表示該測(cè)試類是否被啟用。
TestExecutionCondition 接口也是包含一個(gè) evaluate 方法,只不過參數(shù)類型是 TestExtensionContext,其返回結(jié)果也是 ConditionEvaluationResult 類的對(duì)象。
通過擴(kuò)展的方式禁用的測(cè)試類和方法,可以通過 JVM 參數(shù) junit.conditions.deactivate 來重新啟用,只需要把相應(yīng)的條件類禁用即可。
清單 18 中擴(kuò)展 DisableAPITests 實(shí)現(xiàn)了 ContainerExecutionCondition 和 TestExecutionCondition 接口,當(dāng)測(cè)試類或方法中包含標(biāo)簽 api 時(shí),通過 ConditionEvaluationResult.disabled()表示對(duì)其禁用。
清單 18. 測(cè)試執(zhí)行條件擴(kuò)展示例
public class DisableAPITests implements ContainerExecutionCondition,
TestExecutionCondition {
@Override
public ConditionEvaluationResult evaluate
(final ContainerExtensionContext context) {
return checkTags(context.getTags());
}
@Override
public ConditionEvaluationResult evaluate
(final TestExtensionContext context) {
return checkTags(context.getTags());
}
private ConditionEvaluationResult checkTags
(final Set<String> tags) {
if (tags.contains("api")) {
return ConditionEvaluationResult.disabled("No API tests!");
}
return ConditionEvaluationResult.enabled("");
}
}
清單 19 中的測(cè)試類的 simpleAPITest 方法使用了標(biāo)簽 api,在執(zhí)行時(shí)會(huì)被禁用。
清單 19. 使用 DisableAPITests 的測(cè)試用例
@ExtendWith(DisableAPITests.class)
public class APITests {
@Test
@Tag("api")
public void simpleAPITest() {
System.out.println("simple API test");
}
}
后處理測(cè)試實(shí)例
通過 TestInstancePostProcessor 可以對(duì)測(cè)試實(shí)例添加后處理的邏輯,從而進(jìn)一步對(duì)實(shí)例進(jìn)行定制,比如可以通過依賴注入的方式來設(shè)置其中的屬性,或是添加額外的初始化邏輯等。
在清單 20 中,擴(kuò)展 InjectAPIEnv 實(shí)現(xiàn)了 TestInstancePostProcessor 接口,在 postProcessTestInstance 方法中通過 Commons Lang 中的 MethodUtils.invokeMethod 來調(diào)用當(dāng)前測(cè)試實(shí)例中的 setEnv 方法,并設(shè)置為 DEV。
清單 20. 后處理測(cè)試實(shí)例的示例
public class InjectAPIEnv implements TestInstancePostProcessor {
@Override
public void postProcessTestInstance
(final Object testInstance,
final ExtensionContext context) throws Exception {
MethodUtils.invokeMethod(testInstance, "setEnv", "DEV");
}
}
清單 21 中給出了使用該擴(kuò)展的示例。
清單 21. 使用后處理測(cè)試實(shí)例的示例
@ExtendWith(InjectAPIEnv.class)
public class APITests {
private String env;
public void setEnv(final String env) {
this.env = env;
}
@Test
public void showInjected() {
assertEquals("DEV", this.env);
}
}
參數(shù)解析
在之前介紹 JUnit 5 的參數(shù)解析時(shí),提到了 JUnit 5 可以自動(dòng)解析 TestInfo 和 TestReporter 類型的參數(shù)。除了這兩種類型的參數(shù)之外,也可以通過擴(kuò)展 ParameterResolver 接口來提供自定義的參數(shù)解析功能。ParameterResolver 接口中有兩個(gè)方法,分別是 supports 和 resolve。兩個(gè)方法的參數(shù)是一樣的,分別是 ParameterContext 和 ExtensionContext 接口的對(duì)象。通過 ParameterContext 可以獲取到需要解析的參數(shù)的信息,而 ExtensionContext 接口可以獲取到當(dāng)前測(cè)試類或方法的上下文信息。