<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-junit5</artifactId>
<version>5.0.1</version>
</dependency>
Vert.x JUnit 5 集成
该模块为使用 JUnit 5 编写 Vert.x 测试提供了集成和支持。
在您的构建中使用它
要使用此组件,请将以下依赖项添加到构建描述符的依赖项部分
-
Maven(在您的
pom.xml
中)
-
Gradle(在您的
build.gradle
文件中)
compile io.vertx:vertx-junit5:5.0.1
为什么异步代码的测试有所不同
测试异步操作需要的工具比 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 -> {
// (we can check here if the server started or not)
});
}
}
这里存在问题,因为 listen
在尝试异步启动 HTTP 服务器时不会阻塞。我们不能简单地假设服务器在 listen
调用返回后已经正确启动。此外
-
传递给
listen
的回调将在 Vert.x 事件循环线程中执行,这与运行 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 ,或者使用下面解释的检查点。 |
存在多个成功条件时的检查点
许多测试可以通过在执行的某个点简单调用 completeNow
来标记为通过。话虽如此,也有许多情况是测试的成功取决于需要验证的不同异步部分。
您可以使用检查点来标记一些执行点为已通过。一个 Checkpoint
可以要求一次标记,或多次标记。当所有检查点都已标记时,相应的 VertxTestContext
会使测试通过。
这是一个使用检查点的示例,检查点用于 HTTP 服务器启动、10 个 HTTP 请求已响应以及 10 个 HTTP 客户端请求已发出
Checkpoint serverStarted = testContext.checkpoint();
Checkpoint requestsServed = testContext.checkpoint(10);
Checkpoint responsesReceived = testContext.checkpoint(10);
vertx.createHttpServer()
.requestHandler(req -> {
req.response().end("Ok");
requestsServed.flag();
})
.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.flag();
})));
}
}));
检查点应该只从测试用例的主线程创建,而不是从 Vert.x 异步事件回调中创建。 |
与 JUnit 5 的集成
与之前的版本相比,JUnit 5 提供了一个不同的模型。
测试方法
Vert.x 集成主要是通过使用 VertxExtension
类,以及使用 Vertx
和 VertxTestContext
实例的测试参数注入来完成的
@ExtendWith(VertxExtension.class)
class SomeTest {
@Test
void some_test(Vertx vertx, VertxTestContext testContext) {
// (...)
}
}
Vertx 实例未集群并具有默认配置。如果您需要其他配置,则不要在该参数上使用注入,并自行准备一个 Vertx 对象。 |
测试会自动围绕 VertxTestContext
实例的生命周期进行包装,因此您无需自己插入 awaitCompletion
调用
@ExtendWith(VertxExtension.class)
class SomeTest {
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 {
// Deploy the verticle and execute the test methods when the verticle
// is successfully deployed
@BeforeEach
void deploy_verticle(Vertx vertx, VertxTestContext testContext) {
vertx.deployVerticle(new HttpServerVerticle()).onComplete(testContext.succeedingThenComplete());
}
HttpClient client;
// Repeat this test 3 times
@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("Deploy a verticle")
void prepare(Vertx vertx, VertxTestContext testContext) {
vertx.deployVerticle(new SomeVerticle()).onComplete(testContext.succeedingThenComplete());
}
@Test
@DisplayName("A first test")
void foo(Vertx vertx, VertxTestContext testContext) {
// (...)
testContext.completeNow();
}
@Test
@DisplayName("A second test")
void bar(Vertx vertx, VertxTestContext testContext) {
// (...)
testContext.completeNow();
}
@AfterEach
@DisplayName("Check that the verticle is still there")
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()
创建,使用 Vertx
的默认设置。但是,您可以配置 VertxOptions
以满足您的需求。一个典型的用例是“扩展阻塞超时警告以进行调试”。要配置 Vertx
对象,您必须
-
创建包含 JSON 格式 的
VertxOptions
的 JSON 文件 -
创建指向该文件的环境变量
VERTX_PARAMETER_FILENAME
,或系统属性vertx.parameter.filename
如果同时存在,环境变量值优先于系统属性值。 |
扩展超时示例文件内容
{
"blockedThreadCheckInterval" : 5,
"blockedThreadCheckIntervalUnit" : "MINUTES",
"maxEventLoopExecuteTime" : 360,
"maxEventLoopExecuteTimeUnit" : "SECONDS"
}
满足这些条件后,Vertx
对象将使用配置的选项创建
关闭和移除 Vertx
对象
注入的 Vertx
对象会自动关闭并从其相应的范围内移除。
例如,如果一个 Vertx
对象是为测试方法的范围创建的,它会在测试完成后关闭。类似地,当它由 @BeforeEach
方法创建时,它会在可能的 @AfterEach
方法完成后关闭。
支持额外的参数类型
Vert.x JUnit 5 扩展是可扩展的:您可以通过 VertxExtensionParameterProvider
服务提供者接口添加更多类型。
如果您使用 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 上,您可以找到一个不断增长的 vertx-junit5
扩展集合,这些扩展与 reactiverse-junit5-extensions
项目中的 Vert.x 堆栈集成:https://github.com/reactiverse/reactiverse-junit5-extensions。
参数顺序
有时,一个参数类型必须放在另一个参数之前。例如,reactiverse-junit5-extensions
项目中的 Web 客户端支持要求 Vertx
参数位于 WebClient
参数之前。这是因为 Vertx
实例需要存在才能创建 WebClient
。
期望参数提供者抛出有意义的异常,以告知用户可能的顺序限制。
无论如何,最好将 Vertx
参数放在首位,然后是按照您手动创建它们所需的顺序排列的后续参数。
使用 @MethodSource
的参数化测试
您可以在 vertx-junit5 中使用 @MethodSource
进行参数化测试。因此,您需要在方法定义中将方法源参数声明在 vertx 测试参数之前。
@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) {
// your test code
testContext.completeNow();
}
}
其他 ArgumentSources
也适用。请参阅 ParameterizedTest
的 API 文档中 Formal Parameter List
部分
在 Vert.x 上下文中运行测试
默认情况下,调用测试方法的线程是 JUnit 线程。可以使用 RunTestOnContext
扩展来改变此行为,通过在 Vert.x 事件循环线程上运行测试方法。
请记住,在使用此扩展时,您不得阻塞事件循环。 |
为此,该扩展需要一个 Vertx
实例。默认情况下,它会自动创建一个,但您可以提供配置选项或供应商方法。
Vertx
实例可以在测试运行时检索。
@ExtendWith(VertxExtension.class)
class RunTestOnContextExampleTest {
@RegisterExtension
RunTestOnContext rtoc = new RunTestOnContext();
Vertx vertx;
@BeforeEach
void prepare(VertxTestContext testContext) {
vertx = rtoc.vertx();
// Prepare something on a Vert.x event-loop thread
// The thread changes with each test instance
testContext.completeNow();
}
@Test
void foo(VertxTestContext testContext) {
// Test something on the same Vert.x event-loop thread
// that called prepare
testContext.completeNow();
}
@AfterEach
void cleanUp(VertxTestContext testContext) {
// Clean things up on the same Vert.x event-loop thread
// that called prepare and foo
testContext.completeNow();
}
}