
文章目录

5.4 测试 Reactive 代码
测试是响应式编程开发过程中不可或缺的重要环节,但由于响应式流的异步特性和复杂的时间依赖关系,传统的测试方法往往难以适用。本章将深入探讨如何有效测试Reactive代码,重点介绍StepVerifier的使用技巧和模拟异步数据流的各种方法。

5.4.1 StepVerifier(验证流)
StepVerifier核心原理
StepVerifier是Project Reactor提供的专门用于测试响应式流的工具类,其核心工作原理如下:
- 订阅控制:接管对Publisher的订阅过程,允许测试代码控制订阅时机
- 请求管理:精确控制背压请求的数量,模拟各种消费场景
- 事件验证:提供丰富的断言方法验证元素、完成和错误信号
- 时间控制:支持虚拟时间推进,避免实际等待长时间操作
基础验证模式
简单序列验证:
java
StepVerifier.create(Flux.just(1, 2, 3))
.expectNext(1)
.expectNext(2)
.expectNext(3)
.expectComplete()
.verify();
错误流验证:
java
StepVerifier.create(Flux.error(new RuntimeException("Boom!")))
.expectErrorMessage("Boom!")
.verify();
条件验证:
java
StepVerifier.create(Flux.range(1, 5))
.expectNextCount(3) // 验证前3个元素
.expectNextMatches(i -> i > 3) // 自定义断言
.expectNext(5)
.verifyComplete();

高级验证技巧
- 背压控制测试:
java
StepVerifier.create(flux, 0) // 初始不请求数据
.thenRequest(1)
.expectNext(firstItem)
.thenRequest(2)
.expectNext(secondItem, thirdItem)
.thenCancel()
.verify();
- 时间序列验证:
java
StepVerifier.withVirtualTime(() ->
Mono.delay(Duration.ofDays(1)).map(d -> "Done"))
.expectSubscription()
.expectNoEvent(Duration.ofDays(1)) // 验证期间无事件
.expectNext("Done")
.verifyComplete();
- 上下文验证:
java
StepVerifier.create(flux.contextWrite(Context.of("key", "value")))
.expectAccessibleContext()
.contains("key", "value")
.then()
.expectNextCount(1)
.verifyComplete();
- 熔断测试:
java
StepVerifier.create(flux.timeout(Duration.ofMillis(100)))
.thenAwait(Duration.ofMillis(200)) // 推进虚拟时间触发超时
.expectError(TimeoutException.class)
.verify();

复杂场景测试模式
- 并行流测试:
java
StepVerifier.create(Flux.range(1, 100)
.parallel(4)
.runOn(Schedulers.parallel())
.map(i -> i * 2))
.expectNextCount(100)
.recordWith(ArrayList::new)
.thenConsumeWhile(x -> true)
.consumeRecordedWith(results -> {
assertThat(results).hasSize(100);
assertThat(results).containsExactlyInAnyOrder(
IntStream.rangeClosed(1, 100).map(i -> i * 2).boxed().toArray());
})
.verifyComplete();
- 重试机制测试:
java
AtomicInteger attempts = new AtomicInteger();
StepVerifier.create(Flux.error(new RuntimeException())
.doOnError(e -> attempts.incrementAndGet())
.retryWhen(Retry.backoff(3, Duration.ofMillis(100)))
.thenAwait(Duration.ofSeconds(1)) // 确保重试完成
.expectError()
.verify();
assertThat(attempts.get()).isEqualTo(4); // 初始+3次重试
- 组合操作符测试:
java
StepVerifier.create(
Flux.combineLatest(
Flux.interval(Duration.ofMillis(100)).take(3),
Flux.interval(Duration.ofMillis(150)).take(3),
(a, b) -> a + "-" + b))
.thenAwait(Duration.ofSeconds(1))
.expectNext("0-0")
.expectNext("1-0")
.expectNext("1-1")
.expectNext("2-1")
.expectNext("2-2")
.verifyComplete();
调试与问题诊断
- 日志记录:
java
StepVerifier.create(flux.log())
.expectNextCount(1)
.verifyComplete();
- 检查点调试:
java
StepVerifier.create(
flux.map(i -> i * 2)
.checkpoint("afterMap")
.filter(i -> i > 10)
.checkpoint("afterFilter"))
.expectNextCount(1)
.verifyComplete();
- 错误分析:
java
try {
StepVerifier.create(flux)
.expectNext(1)
.expectNext(2)
.verifyComplete();
} catch (AssertionError e) {
// 解析错误详情
ErrorMessageFormatter.format(e).lines().forEach(System.out::println);
}

5.4.2 模拟异步数据流
测试数据源创建
- 静态数据源:
java
Flux<Integer> staticFlux = Flux.just(1, 2, 3);
Mono<String> staticMono = Mono.just("value");
Flux<Long> intervalFlux = Flux.interval(Duration.ofMillis(100)).take(5);
- 动态生成器:
java
Flux<String> generatedFlux = Flux.generate(
() -> 0, // 初始状态
(state, sink) -> {
if (state < 5) {
sink.next("Value " + state);
return state + 1;
} else {
sink.complete();
return state;
}
});
- 基于回调的源:
java
Flux<String> callbackFlux = Flux.create(sink -> {
someAsyncApi.registerListener(new AsyncListener() {
@Override
public void onData(String data) {
sink.next(data);
}
@Override
public void onComplete() {
sink.complete();
}
});
});
高级模拟技术
- 行为模拟(Behavior Simulation):
java
Flux<String> mockFlux = Flux.defer(() -> {
if (System.currentTimeMillis() % 2 == 0) {
return Flux.just("Even", "Time");
} else {
return Flux.error(new IllegalStateException("Odd time"));
}
});
- 状态机模拟:
java
class StatefulEmitter {
private int count = 0;
Flux<String> emit() {
return Flux.generate(sink -> {
if (count++ < 3) {
sink.next("Event " + count);
} else {
sink.error(new RuntimeException("Max events reached"));
}
});
}
}

- 时间敏感模拟:
java
Flux<Long> timedFlux = Flux.interval(Duration.ofMillis(100))
.map(i -> {
if (i == 5) throw new RuntimeException("Boom at 5");
return i;
})
.onErrorResume(e -> Flux.interval(Duration.ofMillis(50)).take(3));
使用Mockito进行测试
- 基本Mocking:
java
@Mock
ReactiveRepository<User> repository;
@Test
void testFindUser() {
when(repository.findById(anyLong()))
.thenReturn(Mono.just(new User("test")));
StepVerifier.create(userService.findUser(1L))
.expectNextMatches(user -> "test".equals(user.getName()))
.verifyComplete();
}
- 参数捕获:
java
ArgumentCaptor<Long> idCaptor = ArgumentCaptor.forClass(Long.class);
when(repository.findById(idCaptor.capture()))
.thenReturn(Mono.just(new User("test")));
StepVerifier.create(userService.findUser(123L))
.expectNextCount(1)
.verifyComplete();
assertThat(idCaptor.getValue()).isEqualTo(123L);
- 验证交互:
java
verify(repository, times(1)).findById(123L);
verify(repository, never()).delete(any());

测试异常场景
- 显式错误模拟:
java
Flux<Integer> errorFlux = Flux.range(1, 5)
.map(i -> {
if (i == 3) throw new IllegalArgumentException("Invalid");
return i;
});
StepVerifier.create(errorFlux)
.expectNext(1, 2)
.expectError(IllegalArgumentException.class)
.verify();
- 错误恢复测试:
java
Flux<Integer> resilientFlux = errorFlux.onErrorResume(e -> Flux.just(9, 10));
StepVerifier.create(resilientFlux)
.expectNext(1, 2, 9, 10)
.verifyComplete();
- 重试测试:
java
AtomicInteger attempts = new AtomicInteger();
Flux<Integer> retryFlux = Flux.error(new RuntimeException())
.doOnSubscribe(s -> attempts.incrementAndGet())
.retry(2);
StepVerifier.create(retryFlux)
.expectError()
.verify();
assertThat(attempts.get()).isEqualTo(3);
集成测试策略
- 嵌入式服务器测试:
java
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class IntegrationTest {
@Autowired
WebTestClient webTestClient;
@Test
void testStreamEndpoint() {
webTestClient.get()
.uri("/stream")
.exchange()
.expectStatus().isOk()
.expectBodyList(Integer.class)
.hasSize(10);
}
}
- 数据库交互测试:
java
@Test
void testReactiveRepository() {
StepVerifier.create(repository.findAll())
.recordWith(ArrayList::new)
.expectNextCount(5)
.consumeRecordedWith(users -> {
assertThat(users).extracting(User::getActive).containsOnly(true);
})
.verifyComplete();
}
- 消息队列测试:
java
@Test
void testMessageProcessing() {
TestTopicProducer producer = createTestProducer();
TestTopicConsumer consumer = createTestConsumer();
producer.send(new TestMessage("payload"));
StepVerifier.create(consumer.flux().take(1))
.expectNextMatches(msg -> "payload".equals(msg.getContent()))
.verifyComplete();
}

最佳实践与模式
-
测试金字塔应用:
- 70%单元测试(StepVerifier + 模拟)
- 20%集成测试(WebTestClient + 真实组件)
- 10%端到端测试(完整系统验证)
-
测试代码组织模式:
java
class FluxProcessorTest {
@Test
void normalProcessing() {
// 正常流程测试
}
@Test
void emptyInput() {
// 边界条件测试
}
@Test
void errorHandling() {
// 错误场景测试
}
@ParameterizedTest
@ValueSource(ints = {0, 1, 5, 100})
void parameterizedTesting(int size) {
// 参数化测试
}
}
- 性能敏感测试:
java
@Test
void throughputTest() {
int count = 100_000;
Flux<Integer> highVolume = Flux.range(1, count);
Duration duration = StepVerifier.create(highVolume)
.expectNextCount(count)
.verifyComplete();
assertThat(duration).isLessThan(Duration.ofSeconds(1));
}

常见陷阱与解决方案
-
未触发的验证:
- 问题:忘记调用verify()导致测试不执行
- 解决:始终确保测试链以verify()结束
-
线程相关问题:
- 问题:在错误的线程上执行断言
- 解决:使用StepVerifier的断言方法而非外部断言
-
时间敏感测试不稳定:
- 问题:真实时钟导致测试时快时慢
- 解决:尽可能使用虚拟时间(withVirtualTime)
-
资源泄漏:
- 问题:未释放测试资源
- 解决:确保Scheduler和连接池在@After中清理
-
过度模拟:
- 问题:模拟过多导致测试失真
- 解决:只在必要时模拟外部依赖
通过合理运用StepVerifier和各种模拟技术,开发者可以构建全面可靠的响应式代码测试套件。关键在于理解响应式流的生命周期和异步特性,设计出既能验证功能正确性,又能保证非功能性需求的测试方案。随着系统演进,应持续维护和优化测试代码,使其成为系统稳定性的有力保障。