어제 오늘 내일

[Test] "비동기 코드는 어떻게 테스트해요?" StepVerifier로 스트림 검증하기 본문

IT/SpringBoot

[Test] "비동기 코드는 어떻게 테스트해요?" StepVerifier로 스트림 검증하기

hi.anna 2026. 4. 4. 00:23

 

JUnit으로 동기 코드를 테스트할 때는 결과가 바로 나옵니다.
하지만 Flux.interval(Duration.ofSeconds(1)) 같은 코드는 1초 뒤에 데이터가 나옵니다.
이걸 테스트하려고 Thread.sleep(1000)을 쓰는 순간, 테스트 시간은 늘어지고 코드는 지저분해집니다.

Project Reactor는 이런 문제를 해결하기 위해 StepVerifier라는 강력한 테스트 도구를 제공합니다.


1. StepVerifier가 뭔가요?

리액티브 스트림의 구독자(Subscriber) 역할을 하는 테스트용 객체입니다.
"첫 번째 데이터는 'A'여야 하고, 두 번째는 'B'여야 하고, 마지막엔 성공적으로 끝나야 해"라는 시나리오를 짜고, 실제로 그렇게 흘러가는지 검증합니다.


2. 기본 사용법: 데이터 검증 ()

가장 기본적인 패턴입니다.

@Test
void fluxTest() {
    Flux<String> flux = Flux.just("Apple", "Banana", "Cherry");

    // 1. 테스트 대상(Publisher)을 넣고 생성
    StepVerifier.create(flux)
            // 2. 예상 시나리오 작성
            .expectNext("Apple")   // 첫 번째 값은 Apple이어야 함
            .expectNext("Banana")  // 두 번째 값은 Banana여야 함
            .expectNext("Cherry")  // 세 번째 값은 Cherry여야 함
            // 3. 완료 신호 검증 및 실행 (이걸 호출해야 실제 구독 시작!)
            .verifyComplete(); 
}

만약 순서가 틀리거나 값이 다르면 AssertionError가 발생하며 테스트가 실패합니다.


3. 에러 검증 ()

비즈니스 로직에서 예외가 잘 발생하는지도 중요하죠.

@Test
void errorTest() {
    Flux<String> flux = Flux.just("A", "B")
            .concatWith(Mono.error(new IllegalArgumentException("잘못된 값")));

    StepVerifier.create(flux)
            .expectNext("A")
            .expectNext("B")
            // .expectError() // 그냥 에러가 났는지만 확인
            .expectError(IllegalArgumentException.class) // 특정 에러 클래스인지 확인
            // .expectErrorMessage("잘못된 값") // 에러 메시지 확인
            .verify(); // 에러로 끝나는 건 정상 완료(Complete)가 아니므로 그냥 verify() 사용
}

4. 개수 검증 ()

데이터가 100만 개인데 일일이 expectNext를 100만 번 쓸 순 없습니다.

@Test
void countTest() {
    Flux<Integer> range = Flux.range(1, 100);

    StepVerifier.create(range)
            .expectNextCount(100) // 100개가 들어오는지 확인
            .verifyComplete();
}

5. 시간의 마법사: 가상 시간 () ★핵심★

"1시간 뒤에 알림을 보낸다"는 로직을 테스트하려면 1시간을 기다려야 할까요?
StepVerifier.withVirtualTime을 쓰면 시간을 조작해서 0.1초 만에 테스트할 수 있습니다.

@Test
void timeTest() {
    // 1시간 뒤에 "Alarm!"을 방출하는 Flux
    Mono<String> alarmMono = Mono.delay(Duration.ofHours(1))
            .map(t -> "Alarm!");

    // withVirtualTime 사용!
    StepVerifier.withVirtualTime(() -> alarmMono)
            // 1. "구독하자마자"는 아무 일도 안 일어남
            .expectSubscription() 
            // 2. 아무런 이벤트 없이 1시간이 지났다고 쳐! (시간 점프)
            .thenAwait(Duration.ofHours(1)) 
            // 3. 그러면 알람이 와야지?
            .expectNext("Alarm!")
            .verifyComplete();
}

이 코드는 실행 즉시 끝납니다. Thread.sleep의 완벽한 상위 호환입니다.


6. 꿀팁: 로그 찍어보기 ()

테스트가 왜 실패하는지 모르겠다면, 스트림 중간에 .log()를 넣어보세요.
onNext, request, onComplete 등 모든 신호(Signal)가 콘솔에 찍혀서 디버깅이 쉬워집니다.

Flux.just("A", "B")
    .log() // 여기에 추가!
    .subscribe();

마치며

오늘의 결론입니다.

  1. 비동기 테스트에서 block()은 금지다. (습관 되면 망함)
  2. StepVerifier를 사용하면 데이터의 값, 순서, 에러, 완료 여부를 시나리오대로 검증할 수 있다.
  3. withVirtualTime을 쓰면 긴 시간의 지연 작업도 순식간에 테스트할 수 있다.

이제 여러분은 비동기 코드도 자신 있게 작성하고 검증할 수 있게 되었습니다.
다음 포스팅에서는 이 시리즈의 대미를 장식할 마지막 주제, "이거 모르면 운영하다가 서버 터집니다." WebFlux 도입 전 꼭 알아야 할 주의사항 (BlockHound 사용법과 디버깅)에 대해 알아보겠습니다.
도움이 되셨다면 좋아요와 댓글 부탁드립니다! 😊

반응형
Comments