<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-unit</artifactId>
<version>5.0.1</version>
<scope>test</scope>
</dependency>
Vert.x Unit
异步多语言单元测试。
简介
Vert.x Unit 旨在编写异步单元测试,提供多语言API,并在 JVM 中运行这些测试。Vert.x Unit API 借鉴了现有测试框架(如 JUnit 或 QUnit)的经验,并遵循 Vert.x 的实践。
因此,Vert.x Unit 是测试 Vert.x 应用程序的自然选择。
要使用 Vert.x Unit,请将以下依赖项添加到构建描述符的 dependencies 部分
-
Maven(在您的
pom.xml
中)
-
Gradle(在您的
build.gradle
文件中)
testCompile ${io.vertx}:${vertx-unit}:5.0.1
Vert.x Unit 可以通过不同的方式使用,并在您的代码运行的任何地方运行,这只是以正确的方式报告结果的问题,本示例展示了最基本的测试套件
TestSuite suite = TestSuite.create("the_test_suite");
suite.test("my_test_case", context -> {
String s = "value";
context.assertEquals("value", s);
});
suite.run();
run
方法将执行套件并遍历套件中的所有测试。套件可以失败或通过,如果外部世界不知道测试结果,这并不重要。
执行后,测试套件现在将向控制台报告测试套件的步骤
Begin test suite the_test_suite Begin test my_test Passed my_test End test suite the_test_suite , run: 1, Failures: 0, Errors: 0
reporters
选项配置了套件运行器用于报告测试执行情况的报告器,有关更多信息,请参阅报告部分。
编写测试套件
测试套件是测试用例的命名集合,测试用例是直接执行的回调。套件可以具有在测试用例或测试套件之前和/或之后执行的生命周期回调,这些回调用于初始化或处置测试套件使用的服务。
TestSuite suite = TestSuite.create("the_test_suite");
suite.test("my_test_case_1", context -> {
// Test 1
});
suite.test("my_test_case_2", context -> {
// Test 2
});
suite.test("my_test_case_3", context -> {
// Test 3
});
API 是流畅的,因此测试用例可以链式调用
TestSuite suite = TestSuite.create("the_test_suite");
suite.test("my_test_case_1", context -> {
// Test 1
}).test("my_test_case_2", context -> {
// Test 2
}).test("my_test_case_3", context -> {
// Test 3
});
不保证测试用例的声明顺序,因此测试用例不应依赖于其他测试用例的执行。这种做法被认为是不好的。
Vert.x Unit 提供 before 和 after 回调用于全局设置或清理
TestSuite suite = TestSuite.create("the_test_suite");
suite.before(context -> {
// Test suite setup
}).test("my_test_case_1", context -> {
// Test 1
}).test("my_test_case_2", context -> {
// Test 2
}).test("my_test_case_3", context -> {
// Test 3
}).after(context -> {
// Test suite cleanup
});
方法的声明顺序不重要,示例在测试用例之前声明 before 回调,在测试用例之后声明 after 回调,但它可以在任何地方,只要在运行测试套件之前完成即可。
before 回调在任何测试之前执行,如果它失败,测试套件的执行将停止并报告失败。after 回调是测试套件执行的最后一个回调,除非 before 回调报告了失败。
同样,Vert.x Unit 提供了 beforeEach 和 afterEach 回调,它们做同样的事情,但对每个测试用例执行
TestSuite suite = TestSuite.create("the_test_suite");
suite.beforeEach(context -> {
// Test case setup
}).test("my_test_case_1", context -> {
// Test 1
}).test("my_test_case_2", context -> {
// Test 2
}).test("my_test_case_3", context -> {
// Test 3
}).afterEach(context -> {
// Test case cleanup
});
beforeEach 回调在每个测试用例之前执行,如果它失败,则不执行测试用例并报告失败。afterEach 回调在测试用例回调之后立即执行,除非 beforeEach 回调报告了失败。
断言
Vert.x Unit 提供了 TestContext
对象用于在测试用例中进行断言。context 对象提供了处理断言时常用的方法。
assertEquals
断言两个对象相等,适用于 basic 类型或 json 类型。
suite.test("my_test_case", context -> {
context.assertEquals(10, callbackCount);
});
还有一个重载版本,用于提供消息
suite.test("my_test_case", context -> {
context.assertEquals(10, callbackCount, "Should have been 10 instead of " + callbackCount);
});
通常,每个断言都提供一个重载版本。
assertNotEquals
assertEquals 的对应部分。
suite.test("my_test_case", context -> {
context.assertNotEquals(10, callbackCount);
});
assertNull
断言对象为 null,适用于 basic 类型或 json 类型。
suite.test("my_test_case", context -> {
context.assertNull(null);
});
assertNotNull
assertNull 的对应部分。
suite.test("my_test_case", context -> {
context.assertNotNull("not null!");
});
assertInRange
assertInRange
针对实数。
suite.test("my_test_case", context -> {
// Assert that 0.1 is equals to 0.2 +/- 0.5
context.assertInRange(0.1, 0.2, 0.5);
});
assertTrue 和 assertFalse
断言布尔表达式的值。
suite.test("my_test_case", context -> {
context.assertTrue(var);
context.assertFalse(value > 10);
});
失败
最后但同样重要的是,test 提供了一个 fail 方法,它将抛出断言错误
suite.test("my_test_case", context -> {
context.fail("That should never happen");
// Following statements won't be executed
});
失败可以是之前看到的 string,也可以是 error。error 对象取决于目标语言,对于 Java 或 Groovy,它可以是任何扩展 `Throwable` 的类,对于 JavaScript 是一个 error,对于 Ruby 是一个 Exception。
使用第三方断言框架
也可以使用任何其他断言框架,如流行的 hamcrest 和 assertj。推荐的方法是使用 verify
并在提供的 Handler 中执行断言。通过这种方式,异步测试的终止将得到正确处理。
suite.test("my_test_case", context -> context.verify(v -> {
// Using here Assert from junit, could be assertj, hamcrest or any other
// Even manually throwing an AssertionError.
Assert.assertNotNull("not null!");
Assert.assertEquals(10, callbackCount);
}));
异步测试
前面的示例假设测试用例在其各自的回调之后终止,这是测试用例回调的默认行为。通常,希望在测试用例回调之后终止测试,例如
suite.test("my_test_case", context -> {
Async async = context.async();
eventBus.consumer("the-address", msg -> {
(2)
async.complete();
});
(1)
});
1 | 回调退出但测试用例未终止 |
2 | 总线发出的事件回调终止测试 |
如果未调用 complete 回调,则测试用例会在一定超时后失败。 |
在同一个测试用例中可以创建多个 Async
对象,所有这些对象都必须“完成”才能终止测试。
suite.test("my_test_case", context -> {
HttpClient client = vertx.createHttpClient();
client.request(HttpMethod.GET, 8080, "localhost", "/").onComplete(context.asyncAssertSuccess(req -> {
req.send().onComplete(context.asyncAssertSuccess(resp -> {
context.assertEquals(200, resp.statusCode());
}));
}));
Async async = context.async();
vertx.eventBus().consumer("the-address", msg -> {
async.complete();
});
});
Async 对象也可以在 before 或 after 回调中使用,在 before 回调中实现依赖于一个或多个异步结果的设置可能非常方便
suite.before(context -> {
Async async = context.async();
HttpServer server = vertx.createHttpServer();
server.requestHandler(requestHandler);
server.listen(8080).onComplete(ar -> {
context.assertTrue(ar.succeeded());
async.complete();
});
});
可以等待特定 Async
的完成,类似于 Java 的倒计时锁存器
Async async = context.async();
HttpServer server = vertx.createHttpServer();
server.requestHandler(requestHandler);
server.listen(8080).onComplete(ar -> {
context.assertTrue(ar.succeeded());
async.complete();
});
// Wait until completion
async.awaitSuccess();
这不应该从事件循环中执行! |
Async 也可以使用初始计数值创建,当倒计时通过 countDown
达到零时完成
Async async = context.async(2);
HttpServer server = vertx.createHttpServer();
server.requestHandler(requestHandler);
server.listen(8080).onComplete(ar -> {
context.assertTrue(ar.succeeded());
async.countDown();
});
vertx.setTimer(1000, id -> {
async.complete();
});
// Wait until completion of the timer and the http request
async.awaitSuccess();
在 `async` 上调用 `complete()` 会像往常一样完成异步操作,它实际上将值设置为 `0`。
异步断言
TestContext
提供了有用的方法,为异步测试提供了强大的结构
asyncAssertSuccess
方法返回一个 Handler<AsyncResult<T>>
实例,其行为类似于 Async
,在成功时解析 Async
,在失败时使测试失败并报告失败原因。
Async async = context.async();
vertx.deployVerticle("my.verticle").onComplete(ar -> {
if (ar.succeeded()) {
async.complete();
} else {
context.fail(ar.cause());
}
});
// Can be replaced by
vertx.deployVerticle("my.verticle").onComplete(context.asyncAssertSuccess());
asyncAssertSuccess
方法返回一个 Handler<AsyncResult<T>>
实例,其行为类似于 Async
,在成功时调用委托的 Handler<T>
,在失败时使测试失败并报告失败原因。
AtomicBoolean started = new AtomicBoolean();
Async async = context.async();
vertx.deployVerticle(new AbstractVerticle() {
public void start() throws Exception {
started.set(true);
}
}).onComplete(ar -> {
if (ar.succeeded()) {
context.assertTrue(started.get());
async.complete();
} else {
context.fail(ar.cause());
}
});
// Can be replaced by
vertx.deployVerticle("my.verticle").onComplete(context.asyncAssertSuccess(id -> {
context.assertTrue(started.get());
}));
当 Handler
退出时,异步操作完成,除非在调用期间创建了新的异步操作,这对于链式异步行为可能很方便
Async async = context.async();
vertx.deployVerticle("my.verticle").onComplete(ar1 -> {
if (ar1.succeeded()) {
vertx.deployVerticle("my.otherverticle").onComplete(ar2 -> {
if (ar2.succeeded()) {
async.complete();
} else {
context.fail(ar2.cause());
}
});
} else {
context.fail(ar1.cause());
}
});
// Can be replaced by
vertx.deployVerticle("my.verticle").onComplete(context.asyncAssertSuccess(id ->
vertx.deployVerticle("my_otherverticle").onComplete(context.asyncAssertSuccess())
));
asyncAssertFailure
方法返回一个 Handler<AsyncResult<T>>
实例,其行为类似于 Async
,在失败时解析 Async
,在成功时使测试失败。
Async async = context.async();
vertx.deployVerticle("my.verticle").onComplete(ar -> {
if (ar.succeeded()) {
context.fail();
} else {
async.complete();
}
});
// Can be replaced by
vertx.deployVerticle("my.verticle").onComplete(context.asyncAssertFailure());
asyncAssertFailure
方法返回一个 Handler<AsyncResult<T>>
实例,其行为类似于 Async
,在失败时调用委托的 Handler<Throwable>
,在成功时使测试失败。
Async async = context.async();
vertx.deployVerticle("my.verticle").onComplete(ar -> {
if (ar.succeeded()) {
context.fail();
} else {
context.assertTrue(ar.cause() instanceof IllegalArgumentException);
async.complete();
}
});
// Can be replaced by
vertx.deployVerticle("my.verticle").onComplete(context.asyncAssertFailure(cause -> {
context.assertTrue(cause instanceof IllegalArgumentException);
}));
当 Handler
退出时,异步操作完成,除非在调用期间创建了新的异步操作。
重复测试
当测试随机或不经常失败时(例如竞态条件),多次运行相同的测试以增加测试失败的可能性会很方便。
TestSuite.create("my_suite").test("my_test", 1000, context -> {
// This will be executed 1000 times
});
声明后,beforeEach 和 afterEach 回调将与测试执行的次数一样多次执行。
测试重复是顺序执行的 |
共享对象
TestContext
具有 get
/put
/remove
操作,用于在回调之间共享状态。
在 before 回调期间添加的任何对象都可以在任何其他回调中使用。每个测试用例都将操作共享状态的副本,因此更新将仅对一个测试用例可见。
TestSuite.create("my_suite").before(context -> {
// host is available for all test cases
context.put("host", "localhost");
}).beforeEach(context -> {
// Generate a random port for each test
int port = helper.randomPort();
// Get host
String host = context.get("host");
// Setup server
Async async = context.async();
HttpServer server = vertx.createHttpServer();
server.requestHandler(req -> {
req.response().setStatusCode(200).end();
});
server.listen(port, host).onComplete(ar -> {
context.assertTrue(ar.succeeded());
context.put("port", port);
async.complete();
});
}).test("my_test", context -> {
// Get the shared state
int port = context.get("port");
String host = context.get("host");
// Do request
HttpClient client = vertx.createHttpClient();
client.request(HttpMethod.GET, port, host, "/resource").onComplete(context.asyncAssertSuccess(req -> {
req.send().onComplete(context.asyncAssertSuccess(resp -> {
context.assertEquals(200, resp.statusCode());
}));
}));
});
共享任何对象仅在 Java 中受支持,其他语言只能共享基本类型或 JSON 类型。其他对象应使用该语言的特性进行共享。 |
运行
创建测试套件后,它不会执行,直到调用 run
方法。
suite.run();
测试套件也可以使用指定的 Vertx
实例运行
suite.run(vertx);
当使用 Vertx
实例运行时,测试套件使用 Vert.x 事件循环执行,有关更多详细信息,请参阅事件循环部分。
可以在同一个 verticle 中执行多个测试套件,Vert.x Unit 会等待所有执行的套件完成。 |
测试套件完成
无法假设测试套件何时完成,如果需要在测试套件之后执行某些代码,则应将其放在测试套件的 after 回调中或作为 Completion
的回调
TestCompletion completion = suite.run(vertx);
// Simple completion callback
completion.handler(ar -> {
if (ar.succeeded()) {
System.out.println("Test suite passed!");
} else {
System.out.println("Test suite failed:");
ar.cause().printStackTrace();
}
});
Completion
对象还提供了一个 resolve
方法,该方法接受一个 Promise
对象,此 Promise
将收到测试套件执行的通知
TestCompletion completion = suite.run();
// When the suite completes, the promise is resolved
completion.resolve(startPromise);
这使得可以轻松创建一个测试 verticle,其部署就是测试套件的执行,从而使部署它的代码能够轻松了解成功或失败。
completion 对象也可以像一个锁存器一样使用,以阻塞直到测试套件完成。当运行测试套件的线程与当前线程不同时,应使用此方法
Completion completion = suite.run();
// Wait until the test suite completes
completion.await();
当线程被中断或触发超时时,await
会抛出异常。
awaitSuccess
是一个变体,当测试套件失败时会抛出异常。
Completion completion = suite.run();
// Wait until the test suite succeeds otherwise throw an exception
completion.awaitSuccess();
超时
测试套件的每个测试用例必须在达到特定超时之前执行。默认超时为 2 分钟,可以使用 test options 进行更改
TestOptions options = new TestOptions().setTimeout(10000);
// Run with a 10 seconds time out
suite.run(options);
事件循环
Vert.x Unit 的执行是一个待执行任务列表,每个任务的执行都由前一个任务的完成驱动。这些任务应尽可能利用 Vert.x 事件循环,但这取决于当前的执行上下文(即测试套件是在 main
中执行还是嵌入在 Verticle
中)以及是否配置了 Vertx
实例。
setUseEventLoop
配置事件循环的使用
useEventLoop:null | useEventLoop:true | useEventLoop:false | |
---|---|---|---|
| 使用 Vert.x 事件循环 | 使用 Vert.x 事件循环 | 强制不使用事件循环 |
在 | 使用当前事件循环 | 使用当前事件循环 | 强制不使用事件循环 |
在 main 中 | 不使用事件循环 | 抛出错误 | 不使用事件循环 |
默认的 useEventLoop
值为 null
,这意味着它在可能的情况下将使用事件循环,并在没有可用事件循环时回退到不使用事件循环。
报告
报告是测试套件的重要组成部分,Vert.x Unit 可以配置为使用不同类型的报告器运行。
默认情况下未配置报告器,运行测试套件时,test options 可以提供来配置一个或多个
ReportOptions consoleReport = new ReportOptions().
setTo("console");
// Report junit files to the current directory
ReportOptions junitReport = new ReportOptions().
setTo("file:.").
setFormat("junit");
suite.run(new TestOptions().
addReporter(consoleReport).
addReporter(junitReport)
);
文件报告
报告到文件,必须提供一个 Vertx
实例
- 到
-
file
:
目录名 - 格式
-
simple 或 junit
- 示例
-
file:.
文件报告器将在配置的目录中创建文件,文件将根据执行的测试套件名称和格式命名(即 simple 创建 txt 文件,junit 创建 xml 文件)。
事件总线报告
报告事件到事件总线,必须提供一个 Vertx
实例
- 到
-
bus
:
事件总线地址 - 示例
-
bus:the-address
它允许将测试套件的执行与报告解耦。
通过事件总线发送的消息可以通过 EventBusCollector
收集,并实现自定义报告
EventBusCollector collector = EventBusCollector.create(
vertx,
new ReportingOptions().addReporter(
new ReportOptions().setTo("file:report.xml").setFormat("junit")));
collector.register("the-address");
Vert.x 集成
默认情况下,断言和失败必须在 TestContext
上完成,并且抛出断言错误仅在 Vert.x Unit 调用时有效
suite.test("my_test_case", ctx -> {
// The failure will be reported by Vert.x Unit
throw new RuntimeException("it failed!");
});
在常规的 Vert.x 回调中,失败将被忽略
suite.test("test-server", testContext -> {
HttpServer server = vertx.createHttpServer().requestHandler(req -> {
if (req.path().equals("/somepath")) {
throw new AssertionError("Wrong path!");
}
req.response().end();
});
});
自 Vert.x 3.3 起,可以设置一个全局异常处理器来报告事件循环中未捕获的异常
suite.before(testContext -> {
// Report uncaught exceptions as Vert.x Unit failures
vertx.exceptionHandler(testContext.exceptionHandler());
});
suite.test("test-server", testContext -> {
HttpServer server = vertx.createHttpServer().requestHandler(req -> {
if (req.path().equals("/somepath")) {
throw new AssertionError("Wrong path!");
}
req.response().end();
});
});
异常处理器在 before 阶段设置,TestContext
在每个 before、test 和 after 阶段之间共享。因此,在 before 阶段获得的异常处理器是正确的。
JUnit 集成
尽管 Vert.x Unit 是多语言的且不基于 JUnit,但可以从 JUnit 运行 Vert.x Unit 测试套件或测试用例,从而允许您将测试与 JUnit 以及您的构建系统或 IDE 集成。
@RunWith(VertxUnitRunner.class)
public class JUnitTestSuite {
@Test
public void testSomething(TestContext context) {
context.assertFalse(false);
}
}
VertxUnitRunner
使用 JUnit 注解来内省类并在类之后创建测试套件。方法应该声明一个 TestContext
参数,如果不声明也可以。然而,TestContext
是检索关联的 Vert.x 实例或执行异步测试的唯一方法。
JUnit 集成也适用于 Groovy 语言,使用 io.vertx.groovy.ext.unit.junit.VertxUnitRunner
运行器。
在 Vert.x 上下文中运行测试
默认情况下,调用测试方法的线程是 JUnit 线程。RunTestOnContext
JUnit 规则可用于改变此行为,以便使用 Vert.x 事件循环线程运行这些测试方法。
因此,当测试方法和 Vert.x 处理器之间共享状态时必须小心,因为它们不会在同一个线程上运行,例如在 Vert.x 处理器中增加计数器并在测试方法中断言计数器。解决这个问题的一种方法是使用适当的同步,另一种方法是在 Vert.x 上下文中执行测试方法,该上下文将传播到创建的处理程序。
为此,RunTestOnContext
规则需要一个 Vertx
实例。可以提供此类实例,否则该规则将在内部管理一个实例。当测试运行时可以检索此类实例,这使得该规则也成为管理 Vertx
实例的一种方式。
@RunWith(VertxUnitRunner.class)
public class RunOnContextJUnitTestSuite {
@Rule
public RunTestOnContext rule = new RunTestOnContext();
@Test
public void testSomething(TestContext context) {
// Use the underlying vertx instance
Vertx vertx = rule.vertx();
}
}
该规则可以用 @Rule
或 @ClassRule
注解,前者为每个测试管理一个 Vert.x 实例,后者为类的测试方法管理一个单一的 Vert.x 实例。
请记住,使用此规则时不能阻塞事件循环。使用 CountDownLatch 或类似类时必须小心。 |
超时
Vert.x Unit 的 2 分钟超时可以通过 @Test
注解的 timeout
成员覆盖
public class JunitTestWithTimeout {
@Test(timeout = 1000l)
public void testSomething(TestContext context) {
//...
}
}
对于更全局的配置,可以使用 Timeout
规则
@RunWith(VertxUnitRunner.class)
public class TimeoutTestSuite {
@Rule
public Timeout rule = Timeout.seconds(1);
@Test
public void testSomething(TestContext context) {
//...
}
}
使用 io.vertx.ext.unit.junit.Timeout ,而不是 org.junit.rules.Timeout 。 |
方法级别的超时会覆盖类级别的超时
@RunWith(VertxUnitRunner.class)
public class TimeoutOverwriteTestSuite {
@Rule
public Timeout rule = Timeout.millis(5_000L);
@Test
public void testQuick(TestContext context) {
//...
}
@Test(timeout = 30_000L)
public void testSlow(TestContext context) {
//...
}
}
@Test 超时会覆盖 io.vertx.ext.unit.junit.Timeout 规则,但不会覆盖 org.junit.rules.Timeout 规则。 |
参数化测试
JUnit 提供了有用的 Parameterized
测试,Vert.x Unit 测试可以通过 VertxUnitRunnerWithParametersFactory
这个特定的运行器运行
@RunWith(Parameterized.class)
@Parameterized.UseParametersRunnerFactory(VertxUnitRunnerWithParametersFactory.class)
public class SimpleParameterizedTest {
@Parameterized.Parameters
public static Iterable<Integer> data() {
return Arrays.asList(0, 1, 2);
}
public SimpleParameterizedTest(int value) {
//...
}
@Test
public void testSomething(TestContext context) {
// Execute test with the current value
}
}
参数化测试也可以在 Groovy 中使用 io.vertx.groovy.ext.unit.junit.VertxUnitRunnerWithParametersFactory
完成。
重复测试
当测试随机或不经常失败时(例如竞态条件),多次运行相同的测试以增加测试失败的可能性会很方便。
使用 JUnit 时,测试必须用 @Repeat
注解才能重复执行。测试还必须在其规则中定义 RepeatRule
。
@RunWith(VertxUnitRunner.class)
public class RepeatingTest {
@Rule
public RepeatRule rule = new RepeatRule();
@Repeat(1000)
@Test
public void testSomething(TestContext context) {
// This will be executed 1000 times
}
}
声明后,before 和 after 生命周期将与测试执行的次数一样多次执行。
测试重复是顺序执行的 |
与其他断言库一起使用
Vert.x Unit 的可用性在 Vert.x 3.3 中得到了极大改进。您现在可以使用 Hamcrest、AssertJ、Rest Assured 或任何您想要的断言库编写测试。这得益于Vert.x 集成中描述的全局异常处理器。
您可以在 vertx-examples 项目中找到使用 Vert.x Unit 和 Hamcrest、AssertJ 的 Java 示例。
Java 语言集成
测试套件集成
Java 语言提供了类,可以使用以下映射规则直接从 Java 类创建测试套件
testSuiteObject
参数方法被检查,带有 TestContext
参数的公共非静态方法被保留,并通过方法名称映射到 Vert.x Unit 测试套件
-
before
: 前置回调 -
after
: 后置回调 -
beforeEach
: 每个测试前回调 -
afterEach
: 每个测试后回调 -
当名称以 test 开头时:根据方法名称命名的测试用例回调
public class MyTestSuite {
public void testSomething(TestContext context) {
context.assertFalse(false);
}
}
这个类可以很容易地转换为 Vert.x 测试套件
TestSuite suite = TestSuite.create(new MyTestSuite());