Vert.x Junit 5 整合
本模块提供了用 JUnit 5 编写 Vert.x 测试的相关整合以及支持。
为什么测试异步代码与平常不同
测试异步操作需要比 JUnit 之类的测试工具更多的工具。 让我们考虑一个典型的例子,用 Vert.x 创建 HTTP 服务,并将它放入 Junit 测试:
@ExtendWith(VertxExtension.class)
class ATest {
Vertx vertx = Vertx.vertx();
@Test
void start_server() {
vertx.createHttpServer()
.requestHandler(req -> req.response().end("Ok"))
.listen(16969, ar -> {
//(在此处检查服务是否已经启动)
});
}
}
这里会有一些问题,因为HTTP服务是异步创建的, listen
方法并不阻塞。
我们不能按常规方式来断定在 listen
方法返回之后,服务就成功启动了。
况且:
-
传到
listen
的回调函数是在 Vert.x event loop 线程上运行的,而不是在执行 Junit 测试的线程上运行的,此外 -
调用
listen
方法之后,单元测试就退出了,并且已经被认为执行成功,此时HTTP服务有可能并没有启动完毕,而且 -
因为
listen
中的回调函数不是运行于单元测试线程上,而是运行于另一线程,所以类似于断言失败等等任何异常都不会被Junit执行器捕获到。
异步执行过程的测试上下文
首先对该模块有贡献的是 VertxTestContext
对象:
-
允许等待其他线程中正在执行的操作,从而触发完成事件。
-
支持接收失败断言用以标记单元测试失败。
这是一个很基础的用法:
@ExtendWith(VertxExtension.class)
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();
}
}
}
-
succeedingThenComplete
返回一个异步结果处理器,该处理器期望成功结果并使 test context 通过。 -
awaitCompletion
具有java.util.concurrent.CountDownLatch
的语义,并且,如果测试通过之前超时了,会返回false
值。 -
如果 context 捕获到了一个错误(潜在的异步错误),那么在测试完成之后,我们必须抛出该异常并让测试用例失败。
使用其他任何断言库
本模块并不要求您使用特定的断言库。 您可以使用原始的JUnit断言、 AssertJ 、等等。
想要在异步代码当中做断言并确定 VertxTestContext
已被潜在失败所通知, 那么您需要将他们包装到 verify
、 succeeding
或者 failing
当中:
HttpClient 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>>
处理器,该处理器期望得到成功结果并完成 test context。 -
failingThenComplete
提供一个Handler<AsyncResult<T>>
处理器,该处理器期望得到失败结果并完成 test context。 -
succeeding
用来提供Handler<AsyncResult<T>>
处理器,该处理器期望成功结果并将该结果传到下一个回调函数中,该过程中从回调函数抛出任何异常都会被认为测试用例失败。 -
failing
用来提供Handler<AsyncResult<T>>
处理器 ,该处理器期望一个失败结果,并将异常传入下一个回调函数中,该过程中从回调函数抛出任何异常都会被认为测试用例失败。 -
verify
提供断言功能,代码块中抛出的任何异常都被认为测试用例失败。
警告
|
与 succeedingThenComplete 和 failingThenComplete 不同,调用 succeeding 或 failing 方法只能让测试用例失败(例如,succeeding 获取到了失败的异步结果)。
如果想让测试用例通过,您仍然需要调用 completeNow ,或者使用下述的 checkpoints 。
|
有多个成功条件的Checkpoint
在一些特定的执行点调用 completeNow
可以轻松的将许多测试标记为通过。
也就是说基于不同的异步结果,一个测试用例在很多种情况下都可以视为成功。
您可以用 checkpoint 以标记某些执行点为通过。
一个 Checkpoint
可以由单个标记或多个标记来控制。
当所有的 checkpoint 被标记后, VertxTestContext
将通过测试用例。
以下是一个结合 checkpoint 启动 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();
HttpClient 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();
})));
}
}));
提示
|
checkpoint只能在测试用例的主线程创建,不能在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 参数,需要您自己提供 Vertx 对象。
|
测试用例会被自动的包装到 VertxTestContext
生命周期,所以您无需自行注入 awaitCompletion
:
@ExtendWith(VertxExtension.class)
class SomeTest {
@Test
void http_server_check_response(Vertx vertx, VertxTestContext testContext) {
vertx.deployVerticle(new HttpServerVerticle(), testContext.succeeding(id -> {
HttpClient 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 并在部署成功之后
// 执行测试用例方法
@BeforeEach
void deploy_verticle(Vertx vertx, VertxTestContext testContext) {
vertx.deployVerticle(new HttpServerVerticle(), testContext.succeedingThenComplete());
}
// 重复测试3次
@RepeatedTest(3)
void http_server_check_response(Vertx vertx, VertxTestContext testContext) {
HttpClient 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(), 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
修饰的方法时都会随之创建一个新的 VertxTestContext
对象。
Vertx
对象的作用范围
Vertx
对象的作用范围取决于声明周期函数在 JUnit中相对执行顺序 里第一个创建 Vertx
的那个方法。一般来说,我们遵循JUnit扩展作用范围规则,
但是这里有一些规范。
-
如果一个父级test context已经持有一个
Vertx
对象, 那么该Vertx
对象在子级扩展的test context中会被复用。 -
在
@BeforeAll
修饰的方法中注入的Vertx
对象,会在之后所有的测试方法以及生命周期函数中注入的Vertx
参数当中共享。 -
在
@BeforeEach
修饰的且没有父级context的方法注入过程中,或者在先前的@BeforeAll
方法的参数注入过程中,会创建一个新的对象,并共享于相关所有的测试方法以及AfterEach
方法。 -
当执行测试方法之前没有创建
Vertx
对象时,则会创建一个新的Vertx
对象(仅仅作用于该方法本身)
配置 Vertx
实例
默认情况下,Vertx
对象使用 Vertx.vertx()
创建,并使用 Vertx
的默认设置。
但是,您可以配置 VertxOptions
以满足您的需要。
一个典型的应用场景是“扩展调试时阻塞超时警告”。
为配置 Vertx
对象,您必须:
-
创建一个带有 json 格式 的
VertxOptions
的 json 文件 -
创建一个环境变量
vertx.parameter.filename
指向该文件
延长超时的配置文件示例:
{
"blockedThreadCheckInterval" : 5,
"blockedThreadCheckIntervalUnit" : "MINUTES",
"maxEventLoopExecuteTime" : 360,
"maxEventLoopExecuteTimeUnit" : "SECONDS"
}
当满足这些条件时, Vertx
对象创建时将使用配置中的参数
对于其他额外参数类型的支持
Vert.x Unit 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
。
对于响应式库,您可以找到许多 vertx-junit5
的扩展库,
他们属于 reactiverse-junit5-extensions
项目,而且都整合了Vert.x技术栈,这些扩展库也正在进一步的发展:
参数顺序
在某些情况下,可能必须将一个参数类型放置在另一个参数之前。
例如 Web Client 在 vertx-junit5-extensions
项目中会要求 Vertx
参数在 WebClient
参数之前。
这是因为只有 Vertx
参数存在的时候,才可以创建 WebClient
。
我们期望参数提供者抛出一些有意义的异常来让用户知道参数顺序的要求。
然而,任何情况下,我们都建议:将 Vertx
作为第一个参数,并按照您创建的顺序去声明后续参数。
用 @MethodSource
做参数化测试
您可以结合vertx-junit5,用 @MethodSource
做参数化测试。因此,您需要在方法定义的vertx测试参数之前声明 method source 参数。
@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
文档的 Formal Parameter List
一节。
在 Vert.x 的 context 中执行测试
默认情况下,是由 JUnit 线程来调用测试方法。
可以使用 RunTestOnContext
扩展以选择使用一个 Vert.x event-loop 线程来执行测试方法。
小心
|
您需要注意在使用该扩展时不要阻塞事件循环。 |
为了使用 Vert.x 的线程来执行测试,扩展需要您提供一个 Vertx
的实例。
默认情况下,扩展会自动创建一个 Vertx
的实例,但您也可以提供配置参数,或是指定一个方法来提供其实例。
这个 Vertx
的实例可以在执行测试的方法中获取到。
@ExtendWith(VertxExtension.class)
class RunTestOnContextExampleTest {
@RegisterExtension
RunTestOnContext rtoc = new RunTestOnContext();
Vertx vertx;
@BeforeEach
void prepare(VertxTestContext testContext) {
vertx = rtoc.vertx();
// 在 Vert.x 的 event-loop 线程上准备测试资源
// 每次执行测试时该线程都不一样
testContext.completeNow();
}
@Test
void foo(VertxTestContext testContext) {
// 在同一个 Vert.x 的 event-loop 线程上执行测试
// 该测试方法和 prepare 方法使用的是同一个线程
testContext.completeNow();
}
@AfterEach
void cleanUp(VertxTestContext testContext) {
// 在同一个 Vert.x 的 event-loop 线程上清理资源
// 该方法和 prepare 与 foo 方法使用的是同一个线程
testContext.completeNow();
}
}