
리액티브 스트림의 테스트는 단순한 값 검증을 넘어 신호(Signal)의 흐름과 비동기 처리 과정까지 정확하게 검증해야 한다. 이때 가장 강력한 도구가 바로 **Reactor의 StepVerifier와 TestPublisher**이다. 이 장에서는 두 도구의 핵심 개념과 API를 실습을 통해 익히며, 정상 흐름, 예외 처리, 시간 기반 스트림, Context 처리 등 다양한 케이스를 효과적으로 테스트하는 방법을 다룬다.
StepVerifier는 Reactor에서 제공하는 테스트 전용 유틸리티로, Flux나 Mono의 emit 과정과 Signal 흐름(onNext, onComplete, onError 등) 을 단계별로 검증할 수 있다. 단순한 값 확인뿐 아니라, 시간 제어, 예외 흐름, Context 전달 여부 등 고급 시나리오까지 테스트 가능하다.
| 구분 | 메서드 및 기능 설명 |
|---|---|
| 기본 검증 | expectNext(...), expectComplete(), verify() 등으로 일반적인 데이터 흐름과 종료 여부 확인 |
| 예외 검증 | expectError(...), expectErrorMessage(...) 등을 통해 예외 발생 및 메시지 검증 |
| 개수 기반 | expectNextCount(n)으로 정확한 emit 개수만 확인 가능, expectNoEvent(Duration)으로 일정 시간 동안 이벤트 없음 검증 |
| 시간 제어 | withVirtualTime(), advanceTimeBy()를 활용한 가상 시간 테스트 |
| 값 기록/소비 | recordWith(...), consumeRecordedWith(...)로 emit된 값들을 모아서 조건부 검증 수행 |
| Context 검증 | contextWrite(...), expectAccessibleContext()를 통해 Reactor Context 기반 테스트 가능 |
Flux<String> helloFlux = Flux.just("Hello", "Reactor");
StepVerifier.create(helloFlux)
.expectNext("Hello")
.expectNext("Reactor")
.expectComplete()
.verify();
expectNext는 순차적으로 emit되는 데이터를 확인하고,expectComplete는 onComplete 신호가 발생했는지 검증한다.verify()는 전체 스트림을 실행하며 위 단계들을 수행한다.요점: 기본적인 스트림 흐름(데이터 → 종료)을 정확하게 검증할 수 있다.
Flux<Integer> numerator = Flux.just(2, 4, 6, 8, 10);
Flux<Integer> denominator = Flux.just(2, 2, 2, 2, 0);
Flux<Integer> result = numerator.zipWith(denominator, (x, y) -> x / y);
StepVerifier.create(result)
.expectNext(1, 2, 3, 4)
.expectError(ArithmeticException.class)
.verify();
0으로 나누는 순간 ArithmeticException이 발생한다.expectError를 사용해 예외 타입까지 정확히 확인할 수 있다.요점: 예외 발생 위치까지 흐름을 따라가며 예외가 정확히 발생하는지 검증 가능하다.
Flux<Integer> numbers = Flux.range(0, 1000).take(500);
StepVerifier.create(numbers)
.expectNext(0)
.expectNextCount(498)
.expectNext(500) // 존재하지 않는 값 → 실패 유도
.expectComplete()
.verify();
expectNextCount(n)은 값 검증이 아닌 emit 개수만 검증한다.500)으로 의도적으로 테스트 실패를 유도할 수 있다.요점: 대용량 스트림에서 특정 개수만 emit되었는지 빠르게 검증할 수 있다.
StepVerifier.withVirtualTime(() ->
Flux.interval(Duration.ofHours(1)).take(11)
)
.expectSubscription()
.then(() -> VirtualTimeScheduler.get().advanceTimeBy(Duration.ofHours(11)))
.expectNextCount(11)
.expectComplete()
.verify();
VirtualTimeScheduler로 테스트 시간을 가상 이동시켜 빠르게 검증할 수 있다.요점: 시간 지연이 있는 스트림(interval, delayElements)도 테스트 효율적으로 검증 가능하다.
Flux<String> source = Flux.just("A", "B", "C");
StepVerifier.create(source)
.recordWith(ArrayList::new)
.expectNextCount(3)
.consumeRecordedWith(list -> {
assertThat(list).containsExactly("A", "B", "C");
})
.expectComplete()
.verify();
recordWith()로 emit된 값을 리스트에 저장하고,consumeRecordedWith()로 원하는 방식으로 검증 가능하다.요점: 순서가 중요한 데이터 비교, 조건부 검증 등에 적합한 방식이다.
Mono<String> mono = Mono.deferContextual(ctx ->
Mono.just("context value: " + ctx.get("message"))
);
StepVerifier.create(mono.contextWrite(Context.of("message", "Hello from Context")))
.expectAccessibleContext()
.hasKey("message")
.then()
.expectNext("context value: Hello from Context")
.verifyComplete();
ThreadLocal과 달리 명시적으로 이어져야 하며,contextWrite()를 통해 삽입한 Context를 검증할 수 있다.요점: Context 기반 로직 (로그 필터링, 사용자 정보 전파 등)을 정확히 테스트할 수 있다.
TestPublisher는 테스트 상황에서 Signal을 수동으로 발생시킬 수 있는 리액터 전용 도구이다. 특히 다음과 같은 상황에서 유용하다:
Flux.just(...)만으로는 표현하기 어려운 비정상 흐름, 예외 조건, 시간 조건 구성| 메서드 | 설명 |
|---|---|
create() | 기본 동작의 정상적인 Publisher 생성 |
createNonCompliant(...) | Reactor 사양을 위반하는 Publisher 생성 (예: 스펙 위반 테스트) |
emit(...) | 여러 데이터를 동시에 emit |
next(...) | 단일 값 emit |
complete() | onComplete Signal을 발생시킴 |
error(Throwable) | onError Signal을 발생시킴 |
TestPublisher<String> publisher = TestPublisher.create();
Mono<String> result = publisher.mono();
StepVerifier.create(result)
.then(() -> publisher.emit("hello"))
.expectNext("hello")
.expectComplete()
.verify();
TestPublisher가 외부에서 수동으로 Signal을 보내도록 구성하고,활용 예시