[Project Reactor] 13. 리액터 테스트 도구 활용하기: StepVerifier와 TestPublisher

y001·2025년 5월 29일

Reactive Programming

목록 보기
13/30
post-thumbnail

리액티브 스트림의 테스트는 단순한 값 검증을 넘어 신호(Signal)의 흐름비동기 처리 과정까지 정확하게 검증해야 한다. 이때 가장 강력한 도구가 바로 **Reactor의 StepVerifierTestPublisher**이다. 이 장에서는 두 도구의 핵심 개념과 API를 실습을 통해 익히며, 정상 흐름, 예외 처리, 시간 기반 스트림, Context 처리 등 다양한 케이스를 효과적으로 테스트하는 방법을 다룬다.


13.1 StepVerifier를 사용한 테스트

StepVerifier란?

StepVerifier는 Reactor에서 제공하는 테스트 전용 유틸리티로, FluxMonoemit 과정과 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 기반 테스트 가능

1. 기본 테스트 흐름

Flux<String> helloFlux = Flux.just("Hello", "Reactor");

StepVerifier.create(helloFlux)
    .expectNext("Hello")
    .expectNext("Reactor")
    .expectComplete()
    .verify();
  • expectNext는 순차적으로 emit되는 데이터를 확인하고,
  • expectCompleteonComplete 신호가 발생했는지 검증한다.
  • verify()는 전체 스트림을 실행하며 위 단계들을 수행한다.

요점: 기본적인 스트림 흐름(데이터 → 종료)을 정확하게 검증할 수 있다.


2. 예외 흐름 테스트

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를 사용해 예외 타입까지 정확히 확인할 수 있다.

요점: 예외 발생 위치까지 흐름을 따라가며 예외가 정확히 발생하는지 검증 가능하다.


3. 개수 기반 검증

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되었는지 빠르게 검증할 수 있다.


4. 시간 흐름 테스트 (withVirtualTime)

StepVerifier.withVirtualTime(() ->
    Flux.interval(Duration.ofHours(1)).take(11)
)
.expectSubscription()
.then(() -> VirtualTimeScheduler.get().advanceTimeBy(Duration.ofHours(11)))
.expectNextCount(11)
.expectComplete()
.verify();
  • 실제 시간으로는 11시간이 걸리지만,
  • VirtualTimeScheduler로 테스트 시간을 가상 이동시켜 빠르게 검증할 수 있다.

요점: 시간 지연이 있는 스트림(interval, delayElements)도 테스트 효율적으로 검증 가능하다.


5. emit 값 기록 및 비교 (recordWith / consumeRecordedWith)

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()로 원하는 방식으로 검증 가능하다.

요점: 순서가 중요한 데이터 비교, 조건부 검증 등에 적합한 방식이다.


6. Context 검증

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();
  • Reactor의 Context는 ThreadLocal과 달리 명시적으로 이어져야 하며,
  • contextWrite()를 통해 삽입한 Context를 검증할 수 있다.

요점: Context 기반 로직 (로그 필터링, 사용자 정보 전파 등)을 정확히 테스트할 수 있다.


13.2 TestPublisher를 사용한 테스트

TestPublisher란?

TestPublisher는 테스트 상황에서 Signal을 수동으로 발생시킬 수 있는 리액터 전용 도구이다. 특히 다음과 같은 상황에서 유용하다:

  • 일반적인 Flux.just(...)만으로는 표현하기 어려운 비정상 흐름, 예외 조건, 시간 조건 구성
  • 의도적으로 규칙을 어긴 Publisher를 만들어 경계 조건 테스트

주요 메서드 정리

메서드설명
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을 보내도록 구성하고,
  • 내부 흐름이 어떻게 반응하는지 검증한다.

활용 예시

  • 비동기 통신에서 외부 시스템 응답을 모킹하고 싶을 때
  • 특정 조건에서 예외를 유발하고 시스템의 반응을 확인하고 싶을 때

0개의 댓글