Vert.x JUnit 5 集成

该模块为使用 JUnit 5 编写 Vert.x 测试提供了集成和支持。

在您的构建中使用它

要使用此组件,请将以下依赖项添加到构建描述符的依赖项部分

  • Maven(在您的 pom.xml 中)

<dependency>
  <groupId>io.vertx</groupId>
  <artifactId>vertx-junit5</artifactId>
  <version>5.0.1</version>
</dependency>
  • 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 调用返回后已经正确启动。此外

  1. 传递给 listen 的回调将在 Vert.x 事件循环线程中执行,这与运行 JUnit 测试的线程不同,并且

  2. 在调用 listen 后,测试立即退出并被认为是已通过,而 HTTP 服务器可能甚至尚未完成启动,并且

  3. 由于 listen 回调在与执行测试的线程不同的线程上执行,因此任何异常,例如由断言失败抛出的异常,都无法被 JUnit 运行器捕获。

异步执行的测试上下文

该模块的第一个贡献是一个 VertxTestContext 对象,它

  1. 允许等待其他线程中的操作通知完成,并且

  2. 支持接收断言失败以将测试标记为失败。

这是一个非常基本的用法

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 收到潜在故障的通知,您需要将它们包装在对 verifysucceedingfailing 的调用中

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 中有用的方法如下

  • completeNowfailNow 用于通知成功或失败

  • succeedingThenComplete 用于提供预期成功并随后完成测试上下文的 Handler<AsyncResult<T>> 处理器

  • failingThenComplete 用于提供预期失败并随后完成测试上下文的 Handler<AsyncResult<T>> 处理器

  • succeeding 用于提供预期成功并将结果传递给另一个回调的 Handler<AsyncResult<T>> 处理器,从回调中抛出的任何异常都被视为测试失败

  • failing 用于提供预期失败并将异常传递给另一个回调的 Handler<AsyncResult<T>> 处理器,从回调中抛出的任何异常都被视为测试失败

  • verify 用于执行断言,从代码块中抛出的任何异常都被视为测试失败。

succeedingThenCompletefailingThenComplete 不同,调用 succeedingfailing 方法只能使测试失败(例如,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 类,以及使用 VertxVertxTestContext 实例的测试参数注入来完成的

@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 扩展作用域规则,但这里是具体说明。

  1. 如果父测试上下文已有一个 Vertx 实例,则该实例会在子扩展测试上下文中被重用。

  2. @BeforeAll 方法中注入会创建一个新实例,该实例将共享给所有后续测试和生命周期方法的注入。

  3. 在没有父上下文或先前 @BeforeAll 注入的情况下,在 @BeforeEach 中注入会创建一个新实例,该实例与相应的测试和 AfterEach 方法共享。

  4. 在运行测试方法之前不存在实例时,将为该测试(且仅为该测试)创建一个实例。

配置 Vertx 实例

默认情况下,Vertx 对象使用 Vertx.vertx() 创建,使用 Vertx 的默认设置。但是,您可以配置 VertxOptions 以满足您的需求。一个典型的用例是“扩展阻塞超时警告以进行调试”。要配置 Vertx 对象,您必须

  1. 创建包含 JSON 格式VertxOptions 的 JSON 文件

  2. 创建指向该文件的环境变量 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();
  }
}

当用作 @RegisterExtension 实例字段时,为每个测试方法创建一个新的 Vertx 对象和 Context@BeforeEach@AfterEach 方法在此上下文中执行。

当用作 @RegisterExtension 静态字段时,为所有测试方法创建一个单独的 Vertx 对象和 Context@BeforeAll@AfterAll 方法也在此上下文中执行。