Vert.x JUnit 5 integration
This module offers integration and support for writing Vert.x tests with JUnit 5.
Use it in your build
-
groupId
:io.vertx
-
artifactId
:vertx-junit5
-
version
: (current Vert.x release or SNAPSHOT)
Why testing asynchronous code is different
Testing asynchronous operations requires more tools than what a test harness like JUnit provides. Let us consider a typical Vert.x creation of a HTTP server, and put it into a JUnit test:
@ExtendWith(VertxExtension.class)
class ATest {
@Test
void start_server() {
Vertx vertx = Vertx.vertx();
vertx.createHttpServer()
.requestHandler(req -> req.response().end("Ok"))
.listen(16969, ar -> {
// (we can check here if the server started or not)
});
}
}
There are issues here since listen
does not block as it tries to start a HTTP server asynchronously.
We cannot simply assume that the server has properly started upon a listen
invocation return.
Also:
-
the callback passed to
listen
will be executed from a Vert.x event loop thread, which is different from the thread that runs the JUnit test, and -
right after calling
listen
, the test exits and is being considered to be passed, while the HTTP server may not even have finished starting, and -
since the
listen
callback executes on a different thread than the one executing the test, then any exception such as one thrown by a failed assertion cannot be capture by the JUnit runner.
A test context for asynchronous executions
The first contribution of this module is a VertxTestContext
object that:
-
allows waiting for operations in other threads to notify of completion, and
-
supports assertion failures to be received to mark a test as failed.
Here is a very basic usage:
@ExtendWith(VertxExtension.class)
class BTest {
@Test
void start_http_server() throws Throwable {
VertxTestContext testContext = new VertxTestContext();
Vertx vertx = Vertx.vertx();
vertx.createHttpServer()
.requestHandler(req -> req.response().end())
.listen(16969, testContext.completing()); (1)
assertThat(testContext.awaitCompletion(5, TimeUnit.SECONDS)).isTrue(); (2)
if (testContext.failed()) { (3)
throw testContext.causeOfFailure();
}
}
}
-
completing
returns an asynchronous result handler that is expected to succeed and then make the test context pass. -
awaitCompletion
has the semantics of ajava.util.concurrent.CountDownLatch
, and returnsfalse
if the waiting delay expired before the test passed. -
If the context captures a (potentially asynchronous) error, then after completion we must throw the failure exception to make the test fail.
Use any assertion library
This module does not make any assumption on the assertion library that you should be using. You can use plain JUnit assertions, AssertJ, etc.
To make assertions in asynchronous code and make sure that VertxTestContext
is notified of potential failures, you need to wrap them with a call to verify
:
WebClient client = WebClient.create(vertx);
client.get(8080, "localhost", "/")
.as(BodyCodec.string())
.send(testContext.succeeding(response -> testContext.verify(() -> {
assertThat(response.body()).isEqualTo("Plop");
testContext.completeNow();
})));
The useful methods in VertxTestContext
are the following:
-
completeNow
andfailNow
to notify of a success or failure -
completing
to provideHandler<AsyncResult<T>>
handlers that expect a success and then completes the test context -
succeeding
to provideHandler<AsyncResult<T>>
handlers that expect a success, and optionally pass the result to another callback -
failing
to provideHandler<AsyncResult<T>>
handlers that expect a failure, and optionally pass the exception to another callback -
verify
to perform assertions, any exception thrown from the code block is considered as a test failure.
警告
|
Unlike completing() , calling succeeding and failing methods can only make a test fail (e.g., succeeding gets a failed asynchronous result).
To make a test pass you still need to call completeNow , or use checkpoints as explained below.
|
Checkpoint when there are multiple success conditions
Many tests can be marked as passed by simply calling completeNow
at some point of the execution.
That being said there are also many cases where the success of a test depends on different asynchronous parts to be validated.
You can use checkpoints to flag some execution points to be passed.
A Checkpoint
can require a single flagging, or multiple flags.
When all checkpoints have been flagged, then the corresponding VertxTestContext
makes the test pass.
Here is an example with checkpoints on the HTTP server being started, 10 HTTP requests having being responded, and 10 HTTP client requests having been made:
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(8080, ar -> {
if (ar.failed()) {
testContext.failNow(ar.cause());
} else {
serverStarted.flag();
}
});
WebClient client = WebClient.create(vertx);
for (int i = 0; i < 10; i++) {
client.get(8080, "localhost", "/")
.as(BodyCodec.string())
.send(ar -> {
if (ar.failed()) {
testContext.failNow(ar.cause());
} else {
testContext.verify(() -> assertThat(ar.result().body()).isEqualTo("Ok"));
responsesReceived.flag();
}
});
}
提示
|
Checkpoints should be created only from the test case main thread, not from Vert.x asynchronous event callbacks. |
Integration with JUnit 5
JUnit 5 provides a different model compared to the previous versions.
Test methods
The Vert.x integration is primarily done using the VertxExtension
class, and using test parameter injection of Vertx
and VertxTestContext
instances:
@ExtendWith(VertxExtension.class)
class SomeTest {
@Test
void some_test(Vertx vertx, VertxTestContext testContext) {
// (...)
}
}
注意
|
The Vertx instance is not clustered and has the default configuration. If you need something else then just don’t use injection on that parameter and prepare a Vertx object by yourself.
|
The test is automatically wrapped around the VertxTestContext
instance lifecycle, so you don’t need to insert awaitCompletion
calls yourself:
@ExtendWith(VertxExtension.class)
class SomeTest {
void http_server_check_response(Vertx vertx, VertxTestContext testContext) {
vertx.deployVerticle(new HttpServerVerticle(), testContext.succeeding(id -> {
WebClient client = WebClient.create(vertx);
client.get(8080, "localhost", "/")
.as(BodyCodec.string())
.send(testContext.succeeding(response -> testContext.verify(() -> {
assertThat(response.body()).isEqualTo("Plop");
testContext.completeNow();
})));
}));
}
}
You can use it with standard JUnit annotations such as @RepeatedTest
or lifecycle callbacks annotations:
@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(), testContext.completing());
}
// Repeat this test 3 times
@RepeatedTest(3)
void http_server_check_response(Vertx vertx, VertxTestContext testContext) {
WebClient client = WebClient.create(vertx);
client.get(8080, "localhost", "/")
.as(BodyCodec.string())
.send(testContext.succeeding(response -> testContext.verify(() -> {
assertThat(response.body()).isEqualTo("Plop");
testContext.completeNow();
})));
}
}
It is also possible to customize the default VertxTestContext
timeout using the @Timeout
annotation either on test classes or methods:
@ExtendWith(VertxExtension.class)
class SomeTest {
@Test
@Timeout(value = 10, timeUnit = TimeUnit.SECONDS)
void some_test(Vertx vertx, VertxTestContext context) {
// (...)
}
}
Lifecycle methods
JUnit 5 provides several user-defined lifecycle methods annotated with @BeforeAll
, @BeforeEach
, @AfterEach
and @AfterAll
.
These methods can request the injection of Vertx
instances.
By doing so, they are likely to perform asynchronous operations with the Vertx
instance, so they can request the injection of a VertxTestContext
instance to ensure that the JUnit runner waits for them to complete, and report possible errors.
Here is an example:
@ExtendWith(VertxExtension.class)
class LifecycleExampleTest {
@BeforeEach
@DisplayName("Deploy a verticle")
void prepare(Vertx vertx, VertxTestContext testContext) {
vertx.deployVerticle(new SomeVerticle(), testContext.completing());
}
@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);
}
}
Scope of VertxTestContext
objects
Since these objects help waiting for asynchronous operations to complete, a new instance is created for any @Test
, @BeforeAll
, @BeforeEach
, @AfterEach
and @AfterAll
method.
Scope of Vertx
objects
The scope of a Vertx
object depends on which lifecycle method in the JUnit relative execution order first required a new instance to be created.
Generally-speaking, we respect the JUnit extension scoping rules, but here are the specifications.
-
If a parent test context already had a
Vertx
instance, it is being reused in children extension test contexts. -
Injecting in a
@BeforeAll
method creates a new instance that is being shared for injection in all subsequent test and lifecycle methods. -
Injecting in a
@BeforeEach
with no parent context or previous@BeforeAll
injection creates a new instance shared with the corresponding test andAfterEach
method(s). -
When no instance exists before running a test method, an instance is created for that test (and only for that test).
Closing and removal of Vertx
objects
Injected Vertx
objects are being automatically closed and removed from their corresponding scopes.
For instance if a Vertx
object is created for the scope of a test method, it is being closed after the test completes.
Similarly, when it is being created by a @BeforeEach
method, it is being closed after possible @AfterEach
methods have completed.
Warning on multiple methods for the same lifecycle events
JUnit 5 allows multiple methods to exist for the same lifecycle events.
As an example, it is possible to define 3 @BeforeEach
methods on the same test.
Because of asynchronous operations it is possible that the effects of these methods happen concurrently rather than sequentially, which may lead to inconsistent states.
This is a problem of JUnit 5 rather than this module. In case of doubt you may always wonder why a single method can’t be better than many.
Support for RxJava 1 and 2 Vertx instances
Reactive eXtension support in Vert.x is being provided by the vertx-rx-java
and vertx-rx-java2
modules.
They provide shims / wrappers around the Vert.x core APIs, like Vertx
(RxJava 1) and Vertx
(RxJava 2).
Instances of these "Rx-ified" Vertx
classes can be injected in test and lifecycle methods, as in:
@DisplayName("👀 A RxJava 2 + Vert.x test")
@ExtendWith(VertxExtension.class)
class RxJava2Test {
@BeforeEach
void prepare(Vertx vertx, VertxTestContext testContext) {
RxHelper.deployVerticle(vertx, new ServerVerticle())
.subscribe(id -> testContext.completeNow(), testContext::failNow);
}
@Test
@DisplayName("🚀 Start a server and perform requests")
void server_test(Vertx vertx, VertxTestContext testContext) {
Checkpoint checkpoints = testContext.checkpoint(10);
HttpRequest<String> request = WebClient
.create(vertx)
.get(8080, "localhost", "/")
.as(BodyCodec.string());
request
.rxSend()
.repeat(10)
.subscribe(
response -> testContext.verify(() -> {
assertThat(response.body()).isEqualTo("Ok");
checkpoints.flag();
}),
testContext::failNow);
}
class ServerVerticle extends AbstractVerticle {
@Override
public void start(Promise<Void> startFuture) throws Exception {
vertx.createHttpServer()
.requestHandler(req -> {
System.out.println(req.method() + " " + req.uri() + " from " + req.remoteAddress().host());
req.response().end("Ok");
})
.rxListen(8080)
.subscribe(server -> startFuture.complete(), startFuture::fail);
}
}
}