<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-junit5</artifactId>
<version>5.0.12</version>
</dependency>
Vert.x JUnit 5 集成
本模块提供了使用 JUnit 5 编写 Vert.x 单元测试的集成与支持。
在构建中引入依赖
要使用本组件,请在项目构建描述文件的 dependencies 配置节中添加以下依赖:
-
Maven(在
pom.xml中):
-
Gradle(在
build.gradle文件中):
compile io.vertx:vertx-junit5:5.0.12
异步代码测试为何截然不同
测试异步操作所需的工具远超 JUnit 等传统测试框架本身所能提供的范围。 让我们来看一段典型的 Vert.x HTTP 服务器创建代码,并将其放入一个 JUnit 测试中:
class ATest {
Vertx vertx = Vertx.vertx();
@Test
void start_server() {
vertx.createHttpServer()
.requestHandler(req -> req.response().end("Ok"))
.listen(16969).onComplete(ar -> {
// (我们可以在此处检查服务器是否已成功启动)
});
}
}
这段代码存在诸多隐患,因为 listen 尝试异步启动 HTTP 服务器,本身并不会阻塞。
我们无法仅凭 listen 方法调用返回就认定服务器已经正常启动。
此外:
-
传递给
listen的回调函数将在 Vert.x EventLoop 线程中执行,这与执行 JUnit 测试的主线程完全不同; -
在调用
listen后,测试方法会立即退出并被判定为通过,而此时 HTTP 服务器可能根本尚未完成启动; -
由于
listen回调与执行测试的线程不同,回调中抛出的任何异常(例如断言失败)都无法被 JUnit 运行器捕获。
用于异步执行的测试上下文
本模块的核心贡献之一是 VertxTestContext 对象,它具备以下能力:
-
允许等待其他线程中的异步操作通知其完成;
-
支持接收断言失败信息,从而将测试标记为失败。
以下是一个非常基础的使用示例:
class BTest {
Vertx vertx = Vertx.vertx();
@Test
void start_http_server() throws Throwable {
VertxTestContext testContext = new VertxTestContext();
vertx.createHttpServer()
.requestHandler(req -> req.response().end())
.listen(16969)
.onComplete(testContext.succeedingThenComplete()); (1)
assertThat(testContext.awaitCompletion(5, TimeUnit.SECONDS)).isTrue(); (2)
if (testContext.failed()) { (3)
throw testContext.causeOfFailure();
}
}
}
| 1 | succeedingThenComplete 返回一个异步结果处理器,它期望操作成功并在成功后让测试上下文顺利通过。 |
| 2 | awaitCompletion 具有类似于 java.util.concurrent.CountDownLatch 的语义,若在测试通过前等待超时则返回 false。 |
| 3 | 若上下文捕获到(潜在的异步)错误,则在完成后我们必须抛出该失败异常,以便让测试明确失败。 |
自由搭配任意断言库
本模块对你应该使用的断言库没有任何预设限制。 你可以使用原生的 JUnit 断言,也可以使用 AssertJ 等第三方断言库。
若要在异步代码中执行断言并确保 VertxTestContext 能够捕获到潜在的失败,需要将断言包装在对 verify、succeeding 或 failing 的调用中:
client = vertx.createHttpClient();
client.request(HttpMethod.GET, 8080, "localhost", "/")
.compose(req -> req.send().compose(HttpClientResponse::body))
.onComplete(testContext.succeeding(buffer -> testContext.verify(() -> {
assertThat(buffer.toString()).isEqualTo("Plop");
testContext.completeNow();
})));
VertxTestContext 中常用的方法如下:
-
completeNow与failNow:用于显式通知操作成功或失败; -
succeedingThenComplete:提供一个期望成功并在成功后直接完成测试上下文的Handler<AsyncResult<T>>处理器; -
failingThenComplete:提供一个期望失败并在失败后直接完成测试上下文的Handler<AsyncResult<T>>处理器; -
succeeding:提供一个期望成功并将结果传递给后续回调的Handler<AsyncResult<T>>处理器;回调中抛出的任何异常都会被视作测试失败; -
failing:提供一个期望失败并将异常传递给后续回调的Handler<AsyncResult<T>>处理器;回调中抛出的任何异常都会被视作测试失败; -
verify:用于执行断言,代码块中抛出的任何异常都会被视作测试失败。
与 succeedingThenComplete 和 failingThenComplete 不同,调用 succeeding 与 failing 方法本身只能让测试失败(例如 succeeding 接收到了一个失败的异步结果)。若要让测试顺利通过,你仍需调用 completeNow,或者按下文所述使用检查点(Checkpoints)。
|
多成功条件下的检查点(Checkpoint)
许多测试只需在执行过程中的某个点调用 completeNow 即可标记为通过。
尽管如此,也有很多测试的成功取决于多个不同的异步环节全部得到验证。
你可以使用检查点(Checkpoints)来标记某些执行步骤已达标。
当所有检查点均已达成时,对应的 VertxTestContext 就会判定测试通过。
当某个检查点需要多次通过时,还可以从检查点派生出计数门闩(Latch)。
以下是一个包含多个检查点的示例:分别验证 HTTP 服务器已启动、10 个 HTTP 请求已被响应、以及发起了 10 个 HTTP 客户端请求:
Checkpoint serverStarted = testContext.checkpoint();
CountDownLatch requestsServed = testContext.checkpoint().asLatch(10);
CountDownLatch responsesReceived = testContext.checkpoint().asLatch(10);
vertx.createHttpServer()
.requestHandler(req -> {
req.response().end("Ok");
requestsServed.countDown();
})
.listen(8888)
.onComplete(testContext.succeeding(httpServer -> {
serverStarted.flag();
client = vertx.createHttpClient();
for (int i = 0; i < 10; i++) {
client.request(HttpMethod.GET, 8888, "localhost", "/")
.compose(req -> req.send().compose(HttpClientResponse::body))
.onComplete(testContext.succeeding(buffer -> testContext.verify(() -> {
assertThat(buffer.toString()).isEqualTo("Ok");
responsesReceived.countDown();
})));
}
}));
| 检查点应当仅在测试用例的主线程中创建,切勿在 Vert.x 异步事件回调内部动态创建。 |
与 JUnit 5 深度集成
相比早期版本,JUnit 5 引入了全新的扩展模型。
测试方法
Vert.x 与 JUnit 5 的集成主要通过 VertxExtension 类实现,并利用测试参数注入来获取 Vertx 与 VertxTestContext 实例:
@ExtendWith(VertxExtension.class)
class SomeTest {
@Test
void some_test(Vertx vertx, VertxTestContext testContext) {
// (...)
}
}
注入的 Vertx 实例是非集群模式并采用默认配置。如果你需要其它自定义配置,只需不要在该参数上使用注入,自行手动构建 Vertx 对象即可。
|
|
在 Vert.x 5 中,多个客户端资源(例如 这意味着如果某个客户端变得不可达,JVM 可能会将其回收,而 Vert.x 可能会不确定地关闭其底层连接池,甚至此时异步操作可能仍在执行中。 在测试中,当客户端作为局部变量创建且在异步回调完成之前未保留强引用时,就可能发生这种情况。这会导致偶发性失败,如 为避免这种情况,请在测试运行期间保持客户端强引用可达(例如将其存储在类字段中),并在合适时优先进行显式 |
测试执行会自动包装在 VertxTestContext 实例的生命周期内,因此你无需手动插入 awaitCompletion 调用:
@ExtendWith(VertxExtension.class)
class SomeTest {
// 保留为成员变量以避免在异步操作期间被提前 GC/Cleaner 回收 (Vert.x 5)
HttpClient client;
@Test
void http_server_check_response(Vertx vertx, VertxTestContext testContext) {
vertx.deployVerticle(new HttpServerVerticle()).onComplete(testContext.succeeding(id -> {
client = vertx.createHttpClient();
client.request(HttpMethod.GET, 8080, "localhost", "/")
.compose(req -> req.send().compose(HttpClientResponse::body))
.onComplete(testContext.succeeding(buffer -> testContext.verify(() -> {
assertThat(buffer.toString()).isEqualTo("Plop");
testContext.completeNow();
})));
}));
}
}
你可以配合标准 JUnit 注解使用,例如 @RepeatedTest 或生命周期回调注解:
@ExtendWith(VertxExtension.class)
class SomeTest {
// 部署 Verticle,并在 Verticle 成功部署后再执行各测试方法
@BeforeEach
void deploy_verticle(Vertx vertx, VertxTestContext testContext) {
vertx.deployVerticle(new HttpServerVerticle()).onComplete(testContext.succeedingThenComplete());
}
// 保留为成员变量以避免在异步操作期间被提前 GC/Cleaner 回收 (Vert.x 5)
HttpClient client;
// 重复运行该测试 3 次
@RepeatedTest(3)
void http_server_check_response(Vertx vertx, VertxTestContext testContext) {
client = vertx.createHttpClient();
client.request(HttpMethod.GET, 8080, "localhost", "/")
.compose(req -> req.send().compose(HttpClientResponse::body))
.onComplete(testContext.succeeding(buffer -> testContext.verify(() -> {
assertThat(buffer.toString()).isEqualTo("Plop");
testContext.completeNow();
})));
}
}
还可以通过在测试类或测试方法上标注 @Timeout 注解,来自定义默认的 VertxTestContext 超时时长:
@ExtendWith(VertxExtension.class)
class SomeTest {
@Test
@Timeout(value = 10, timeUnit = TimeUnit.SECONDS)
void some_test(Vertx vertx, VertxTestContext context) {
// (...)
}
}
生命周期方法
JUnit 5 提供了多个由用户定义的生命周期方法,标注有 @BeforeAll、@BeforeEach、@AfterEach 和 @AfterAll。
这些方法同样可以请求注入 Vertx 实例。
这样做时,它们很可能需要使用该 Vertx 实例执行异步操作,因此也可以请求注入 VertxTestContext 实例,以确保 JUnit 运行器等待其执行完成并报告可能的错误。
示例如下:
@ExtendWith(VertxExtension.class)
class LifecycleExampleTest {
@BeforeEach
@DisplayName("部署 Verticle")
void prepare(Vertx vertx, VertxTestContext testContext) {
vertx.deployVerticle(new SomeVerticle()).onComplete(testContext.succeedingThenComplete());
}
@Test
@DisplayName("第一个测试")
void foo(Vertx vertx, VertxTestContext testContext) {
// (...)
testContext.completeNow();
}
@Test
@DisplayName("第二个测试")
void bar(Vertx vertx, VertxTestContext testContext) {
// (...)
testContext.completeNow();
}
@AfterEach
@DisplayName("检查 Verticle 是否依然存活")
void lastChecks(Vertx vertx) {
assertThat(vertx.deploymentIDs())
.isNotEmpty()
.hasSize(1);
}
}
VertxTestContext 对象的作用域
由于这些对象用于等待异步操作完成,因此每次针对 @Test、@BeforeAll、@BeforeEach、@AfterEach 和 @AfterAll 方法都会创建全新的实例。
Vertx 对象的作用域
Vertx 对象的作用域取决于按照 JUnit 相对执行顺序 中哪个生命周期方法最先需要创建新实例。
总体而言,我们遵循 JUnit 扩展的作用域规则,具体规范如下:
-
如果父测试上下文中已存在
Vertx实例,则该实例会在子级扩展测试上下文中复用; -
在
@BeforeAll方法中注入会创建一个全新实例,并在后续所有测试与生命周期方法的注入中共享; -
在没有父上下文或先前未发生
@BeforeAll注入的情况下,于@BeforeEach中注入会创建一个新实例,并与对应的测试方法以及AfterEach方法共享; -
当在运行测试方法之前不存在任何实例时,将专为该测试创建实例(且仅作用于该测试)。
配置 Vertx 实例
默认情况下,Vertx 对象使用 Vertx.vertx() 结合 Vert.x 默认设置创建。
但是,你可以根据需求配置 VertxOptions。
一个典型场景是“在调试时延长阻塞超时警告阈值”。
要配置 Vertx 对象,你必须:
-
创建一个包含 JSON 格式 的
VertxOptions文件; -
配置环境变量
VERTX_PARAMETER_FILENAME,或系统属性vertx.parameter.filename,指向该文件。
| 若环境变量与系统属性同时存在,环境变量的取值优先级更高。 |
用于延长超时的 JSON 配置文件示例:
{
"blockedThreadCheckInterval" : 5,
"blockedThreadCheckIntervalUnit" : "MINUTES",
"maxEventLoopExecuteTime" : 360,
"maxEventLoopExecuteTimeUnit" : "SECONDS"
}
满足这些条件后,Vertx 对象便会根据所配置的选项进行创建。
Vertx 对象的关闭与清理
注入的 Vertx 对象会在其对应作用域生命周期结束时自动关闭并清理。
例如,若某个 Vertx 对象是为测试方法的作用域创建的,它会在测试执行完毕后自动关闭。
类似地,当它由 @BeforeEach 方法创建时,会在可能的 @AfterEach 方法全部执行完毕后自动关闭。
支持扩展参数类型
Vert.x JUnit 5 扩展具备出色的可扩展性:你可以通过 VertxExtensionParameterProvider 服务提供者接口(SPI)添加更多参数类型。
如果你使用 RxJava,可以替代 io.vertx.core.Vertx 注入以下类型:
-
io.vertx.rxjava3.core.Vertx,或 -
io.vertx.reactivex.core.Vertx,或 -
io.vertx.rxjava.core.Vertx。
要实现此功能,请在项目中引入对应的依赖库:
-
io.vertx:vertx-junit5-rx-java3,或 -
io.vertx:vertx-junit5-rx-java2,或 -
io.vertx:vertx-junit5-rx-java。
在 Reactiverse 上,你可以找到由 reactiverse-junit5-extensions 项目维护的、日益丰富的 vertx-junit5 扩展集合,用于集成各种 Vert.x 技术栈:
https://github.com/reactiverse/reactiverse-junit5-extensions。
参数顺序规范
在某些情况下,某种参数类型必须置于另一种参数之前。
例如,reactiverse-junit5-extensions 项目中的 Web Client 支持要求 Vertx 参数必须位于 WebClient 参数之前。
这是因为创建 WebClient 必须首先依赖 Vertx 实例的存在。
参数提供者应当抛出清晰明确的异常,告知使用者可能存在的参数顺序约束。
在任何情况下,将 Vertx 参数放在首位,后续参数按照手动创建它们时所需的依赖顺序排列,都是最佳实践。
配合 @MethodSource 执行参数化测试
你可以使用 vertx-junit5 配合 @MethodSource 执行参数化测试。为此,你需要在方法定义中将方法源(MethodSource)参数声明在 Vert.x 测试参数之前。
@ExtendWith(VertxExtension.class)
static class SomeTest {
static Stream<Arguments> testData() {
return Stream.of(
Arguments.of("complex object1", 4),
Arguments.of("complex object2", 0)
);
}
@ParameterizedTest
@MethodSource("testData")
void test2(String obj, int count, Vertx vertx, VertxTestContext testContext) {
// 你的测试逻辑
testContext.completeNow();
}
}
其他 ArgumentSources 也同理适用。
详细说明请参阅
ParameterizedTest API 文档中的 Formal Parameter List 章节。
在 Vert.x 上下文中运行测试
默认情况下,调用测试方法的线程是 JUnit 线程。
可以使用 RunTestOnContext 扩展来改变该行为,直接在 Vert.x EventLoop 线程中运行测试方法。
| 请牢记,在使用此扩展时,绝对不能阻塞 EventLoop 线程。 |
为此,该扩展需要一个 Vertx 实例。
默认情况下它会自动创建一个,但你也可以提供自定义配置选项或 Supplier 提供方法。
在测试运行期间可以检索该 Vertx 实例。
@ExtendWith(VertxExtension.class)
class RunTestOnContextExampleTest {
@RegisterExtension
RunTestOnContext rtoc = new RunTestOnContext();
Vertx vertx;
@BeforeEach
void prepare(VertxTestContext testContext) {
vertx = rtoc.vertx();
// 在 Vert.x EventLoop 线程中准备前置逻辑
// 该线程会随着每个测试实例而变化
testContext.completeNow();
}
@Test
void foo(VertxTestContext testContext) {
// 在调用 prepare 的同一个 Vert.x EventLoop 线程中执行测试
testContext.completeNow();
}
@AfterEach
void cleanUp(VertxTestContext testContext) {
// 在调用 prepare 和 foo 的同一个 Vert.x EventLoop 线程中执行清理
testContext.completeNow();
}
}