본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.

 

 

강의 요약

오늘 강의에서는 WebClient를 활용한 타켓 웹사이트 처리에 대해 다루었다. 다만 Reactive 프로그래밍을 도입하면 테스트 작성이 복잡해지는 트레이드오프가 존재한다. 기존의 동기 코드는 반환값을 직접 검증하면 되지만, Reactive 스트림은 시간에 따라 발생하는 이벤트를 다루기 때문에 테스트 코드 작성에 대해서 알아보자.

StepVerifier를 통한 Reactive 스트림 테스트

reactor-test는 Reactive 스트림을 테스트하기 위한 두 가지 주요 기능을 제공한다. StepVerifier를 통한 단계별 테스트와 TestPublisher를 통한 사전 정의된 데이터 생성이다. 코드에 Publisher가 정의되어 있을 때, 구독 시 어떻게 동작하는지 검증하는 것이 가장 일반적인 사용 사례다.

의존성 설정

<dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-test</artifactId>
    <scope>test</scope>
    <version>3.6.0</version>
</dependency>

기본 사용 방법

StepVerifier API를 사용하면 발행되는 요소와 스트림 완료 시 발생하는 동작에 대한 기대값을 정의할 수 있다. 4글자 이름만 필터링하고 대문자로 변환하는 간단한 Publisher를 생성해보자.

Flux<String> source = Flux.just("John", "Monica", "Mark", "Cloe", "Frank", "Casper", "Olivia", "Emily", "Cate")
    .filter(name -> name.length() == 4)
    .map(String::toUpperCase);

StepVerifier.create(source)
    .expectNext("JOHN")
    .expectNextMatches(name -> name.startsWith("MA"))
    .expectNext("CLOE", "CATE")
    .expectComplete()
    .verify();

  • create 메서드로 StepVerifier 빌더를 생성하고 테스트 대상 Flux를 래핑한다.
  • expectNext(T element)로 첫 번째 신호를 검증하며, 여러 요소를 한 번에 전달할 수 있다.
  • expectNextMatches는 Predicate<T>를 제공해 더 커스텀한 매칭을 수행한다.
  • 마지막으로 스트림이 완료되는 것을 기대하고, verify()로 테스트를 트리거한다.

 

예외 처리 검증

Flux Publisher를 Mono와 연결하여, 구독 시 즉시 에러로 종료되도록 만들 수 있다.

Flux<String> error = source.concatWith(
    Mono.error(new IllegalArgumentException("Our message"))
);

StepVerifier.create(error)
    .expectNextCount(4)
    .expectErrorMatches(throwable ->
        throwable instanceof IllegalArgumentException &&
        throwable.getMessage().equals("Our message")
    )
    .verify();

  • onError 신호는 구독자에게 Publisher가 에러 상태로 종료되었음을 알리므로, 예외를 검증하는 메서드는 하나만 사용해야 하며 그 이후에 추가 기대값을 설정할 수 없다.

 

예외 검증 전용 메서드

메서드 설명
expectError() 모든 종류의 에러 기대
expectError(Class) 특정 타입의 에러 기대
expectErrorMessage(String) 특정 메시지를 가진 에러 기대
expectErrorMatches(Predicate) 주어진 predicate와 일치하는 에러 기대
expectErrorSatisfies(Consumer) Throwable을 소비하여 커스텀 assertion 수행

 

시간 기반 Publisher 테스트

실제 애플리케이션에서 이벤트 간 하루 지연이 있는 Publisher가 있다고 가정하면, 테스트가 하루 동안 실행되는 것을 원하지 않는다. StepVerifier.withVirtualTime은 장시간 실행 테스트를 회피하도록 설계되었다.

StepVerifier.withVirtualTime(() -> Flux.interval(Duration.ofSeconds(1)).take(2))
    .expectSubscription()
    .expectNoEvent(Duration.ofSeconds(1))
    .expectNext(0L)
    .thenAwait(Duration.ofSeconds(1))
    .expectNext(1L)
    .verifyComplete();

  • withVirtualTime은 Flux를 직접 받지 않고 Supplier를 받는다.
  • 이는 스케줄러가 설정된 후 테스트 대상 Flux 인스턴스를 지연 생성하기 위함이다.
  • 코드 앞부분에서 Flux를 인스턴스화한 후 Supplier가 해당 변수를 반환하도록 하면 안 되고, 항상 람다 내부에서 Flux를 인스턴스화해야 한다.

 

시간 관련 메서드

메서드 동작
thenAwait(Duration) 단계 평가를 일시 중지하며, 이 시간 동안 새 이벤트가 발생할 수 있음
expectNoEvent(Duration) 지속 시간 동안 이벤트가 발생하면 실패하며, 주어진 시간 동안 시퀀스 통과
  • 첫 번째 신호는 구독 이벤트이므로, expectNoEvent(Duration)는 항상 expectSubscription() 다음에 와야 한다.

 

실행 후 Assertion

전체 시나리오가 성공적으로 실행된 후 추가 상태를 검증해야 할 때가 있다. 몇 개의 요소를 발행하고 완료한 후, 일시 중지하고 하나의 요소를 더 발행하여 드롭시키는 커스텀 Publisher를 만들어보자.

Flux<Integer> source = Flux.<Integer>create(emitter -> {
    emitter.next(1);
    emitter.next(2);
    emitter.next(3);
    emitter.complete();
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    emitter.next(4);
}).filter(number -> number % 2 == 0);

StepVerifier.create(source)
    .expectNext(2)
    .expectComplete()
    .verifyThenAssertThat()
    .hasDropped(4)
    .tookLessThan(Duration.ofMillis(1050));

  • 2를 발행하지만 emitter.complete를 먼저 호출했으므로 4는 드롭된다.
  • verifyThenAssertThat를 사용하면 StepVerifier.Assertions를 반환하며, 여기에 assertion을 추가할 수 있다.

 

TestPublisher를 통한 데이터 생성

선택된 신호를 트리거하기 위해 특별한 데이터가 필요한 경우가 있다. 테스트하려는 매우 특정한 상황이 있거나, 직접 구현한 연산자의 동작을 테스트할 때 TestPublisher<T>를 사용하면 프로그래밍 방식으로 다양한 신호를 트리거할 수 있다.

 

TestPublisher 신호 메서드

메서드 설명
next(T value) 구독자에게 하나 이상의 신호 전송
emit(T value) next(T)와 동일하지만 이후 complete() 호출
complete() complete 신호로 소스 종료
error(Throwable) 에러로 소스 종료
flux() TestPublisher를 Flux로 래핑하는 편의 메서드
mono() TestPublisher를 Mono로 래핑
  • 간단한 TestPublisher를 생성하여 몇 개의 신호를 발행하고 예외로 종료할 수 있다.
TestPublisher.<String>create()
    .next("First", "Second", "Third")
    .error(new RuntimeException("Message"));

TestPublisher 실전 활용

  • 특정 상황과 밀접하게 일치하는 신호를 트리거하고 싶을 때, 데이터 소스를 완전히 제어하는 것이 중요하다.
  • Flux<String>를 생성자 파라미터로 받아 getUpperCase() 연산을 수행하는 클래스를 만들어보자.
class UppercaseConverter {
    private final Flux<String> source;

    UppercaseConverter(Flux<String> source) {
        this.source = source;
    }

    Flux<String> getUpperCase() {
        return source.map(String::toUpperCase);
    }
}

  • UppercaseConverter가 복잡한 로직과 연산자를 가진 클래스이고, 소스 Publisher로부터 매우 특정한 데이터를 공급해야 한다고 가정하면, TestPublisher로 쉽게 달성할 수 있다.
TestPublisher<String> testPublisher = TestPublisher.create();
UppercaseConverter uppercaseConverter = new UppercaseConverter(testPublisher.flux());

StepVerifier.create(uppercaseConverter.getUpperCase())
    .then(() -> testPublisher.emit("aA", "bb", "ccc"))
    .expectNext("AA", "BB", "CCC")
    .verifyComplete();

  • UppercaseConverter 생성자 파라미터에 테스트 Flux Publisher를 생성하고, TestPublisher가 세 개의 요소를 발행하고 완료한다.

 

Misbehaving TestPublisher

createNonCompliant 팩토리 메서드로 규격을 준수하지 않는 TestPublisher를 생성할 수 있다. TestPublisher.Violation enum 값을 생성자에 전달하여 Publisher가 무시할 수 있는 규격 부분을 지정한다.

TestPublisher.createNoncompliant(TestPublisher.Violation.ALLOW_NULL)
    .emit("1", "2", null, "3");

  • null 요소에 대해 NullPointerException을 발생시키지 않는 TestPublisher다.

 

TestPublisher.Violation 옵션

Violation 설명
ALLOW_NULL null 요소 허용
REQUEST_OVERFLOW 불충분한 요청 수가 있을 때 next() 호출 시 IllegalStateException 발생 없이 허용
CLEANUP_ON_TERMINATE 여러 번 연속으로 종료 신호 전송 허용
DEFER_CANCELLATION 취소 신호를 무시하고 요소 발행 계속 허용

 

참고 출처

 

 

 

시작 시간
종료 시간
학습 인증 - 디지털 필기
수강 인증

 

https://fastcampus.info/4oKQD6b

+ Recent posts