1. 개요

이 사용방법(예제)에서는 StepVerifierTestPublisher 를 사용하여 반응형 스트림테스트하는 방법을 자세히 살펴 보겠습니다 .

일련의 리액터 작업을 포함하는 Spring Reactor 애플리케이션을 조사할 것  입니다.

2. 메이븐 의존성

Spring Reactor는 반응성 스트림을 테스트하기 위한 여러 클래스와 함께 제공됩니다.

우리는 react-test  의존성 을 추가 하여 이를 얻을 수 있습니다 .

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

3. 단계 검증자

일반적으로 react-test  에는 두 가지 주요 용도가 있습니다.

  • StepVerifier 로 단계별 테스트 생성
  • 다운스트림 연산자를 테스트하기 위해 TestPublisher  로 미리 정의된 데이터 생성

반응형 스트림을 테스트하는 가장 일반적인 경우는  코드에 정의된 게시자( Flux  또는  Mono )가 있는 경우입니다. 누군가가 구독할 때 어떻게 작동하는지 알고 싶습니다. 

StepVerifier API를 사용하면  기대 하는 요소와 스트림이 완료될 때 발생하는 상황 에 따라 게시된 요소의 기대치를 정의할 수 있습니다  .

먼저 일부 연산자로 게시자를 생성해 보겠습니다.

Flux.just(T 요소)를 사용 합니다.  이 메서드는 주어진 요소를 방출하고 완료 하는 Flux  를 생성합니다.

고급 연산자는 이 문서의 범위를 벗어나므로 대문자로 매핑된 4글자 이름만 출력하는 간단한 게시자를 만듭니다.

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

3.1. 단계별 시나리오

이제 누군가 구독할 때 어떤 일이 발생하는지 테스트하기 위해 StepVerifier 로 소스  를 테스트해 보겠습니다 .

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

먼저 create  메서드를 사용하여 StepVerifier  빌더를  만듭니다 .

다음 으로 테스트 중인 Flux  소스 를 래핑합니다 . 첫 번째 신호는 expectNext(T 요소)로  확인 되지만 실제로 는 여러 요소를  expectNext 로 전달할 수 있습니다 .

우리는 또한 expectNextMatches  를 사용 하고 더 많은 Custom 일치를 위해 Predicate<T>  를 제공 할 수 있습니다.

마지막으로 스트림이 완료될 것으로 기대합니다.

마지막으로 verify() 를 사용  하여 테스트를 트리거합니다 .

3.2. StepVerifier 의 예외

이제 Flux 게시자를 Mono 와 연결해 보겠습니다 .

구독할 때 오류와 함께 Mono  가 즉시 종료되도록 할 것입니다 .

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

이제 4개의 모든 요소 후에 스트림이 예외와 함께 종료될 것으로 예상합니다 .

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

예외를 확인하는 데는 한 가지 방법만 사용할 수 있습니다. OnError 신호  는  게시자가 오류 상태로 종료 되었음을 구독자에게 알립니다 . 따라서 이후에 더 많은 기대를 추가할 수 없습니다 .

예외의 유형과 메시지를 한 번에 확인할 필요가 없다면 다음과 같은 전용 방법 중 하나를 사용할 수 있습니다.

  • expectError()  – 모든 종류의 오류를 예상합니다.
  • expectError(Class<? extends Throwable> clazz ) –  특정 유형의 오류를 예상합니다.
  • expectErrorMessage(String errorMessage) –  특정 메시지가 있는 오류를 예상합니다.
  • expectErrorMatches(Predicate<Throwable> predicate)  – 주어진 술어와 일치하는 오류를 예상합니다.
  • expectErrorSatisfies(Consumer<Throwable> assertionConsumer)  – 사용자 지정 주장을 수행하기 위해 Throwable  을 소비합니다. 

3.3. 시간 기반 게시자 테스트

때때로 게시자는 시간 기반입니다.

예를 들어 실제 애플리케이션에서 이벤트 사이에 하루의 지연이 있다고 가정합니다 . 이제 분명히 우리는 이러한 지연으로 예상되는 동작을 확인하기 위해 하루 종일 테스트를 실행하는 것을 원하지 않습니다.

StepVerifier.withVirtualTime  빌더는 장기 실행 테스트를 방지하도록 설계되었습니다.

withVirtualTime 을 호출하여 빌더를 생성합니다  . 이 방법은 Flux  를 입력으로 사용하지 않습니다. 대신 스케줄러를 설정한 후 테스트된 Flux  의 인스턴스를 느리게 생성하는 Supplier 를 사용합니다.

이벤트 간의 예상 지연을 테스트하는 방법을 보여주기 위해 2초 동안 실행되는 1초 간격의 Flux  를 생성해 보겠습니다. 타이머가 올바르게 실행되면 두 가지 요소만 가져와야 합니다.

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

코드 초반에 Flux  를 인스턴스화한 다음 Provider  가 이 변수를 반환 하도록 하는 것을 피해야 합니다. 대신 항상 람다 내부에서 Flux  를 인스턴스화해야 합니다 .

시간을 다루는 두 가지 주요 예상 방법이 있습니다.

  • thenAwait(Duration duration) –  단계 평가를 일시 중지합니다. 이 시간 동안 새로운 이벤트가 발생할 수 있습니다.
  • expectNoEvent(Duration duration) – 기간 동안  이벤트 가 나타나면 실패합니다 . 시퀀스는 주어진 기간 으로 전달됩니다

첫 번째 신호는 구독 이벤트이므로 모든  expectNoEvent(Duration duration)  앞에는  expectSubscription() 이 와야 합니다.

3.4. StepVerifier 를 사용한 사후 실행 어설션

따라서 지금까지 살펴본 바와 같이 우리의 기대치를 단계별로 설명하는 것은 간단합니다.

그러나 전체 시나리오가 성공적으로 실행된 후 추가 상태를 확인해야 하는 경우가 있습니다.

사용자 지정 게시자를 만들어 보겠습니다. 몇 가지 요소를 내보낸 다음 완료하고 일시 중지한 다음 요소를 하나 더 내보냅니다 .

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);

먼저 emitter.complete 를 호출했기 때문에 2를 방출하지만 4를 떨어뜨릴 것으로 예상합니다 .

따라서 verifyThenAssertThat  을 사용하여 이 동작을 확인  하겠습니다. 이 메서드는  어설션을 추가할 수 있는 StepVerifier.Assertions  를 반환합니다.

@Test
public void droppedElements() {
    StepVerifier.create(source)
      .expectNext(2)
      .expectComplete()
      .verifyThenAssertThat()
      .hasDropped(4)
      .tookLessThan(Duration.ofMillis(1050));
}

4. TestPublisher 로 데이터 생성

때로는 선택한 신호를 트리거하기 위해 특별한 데이터가 필요할 수 있습니다.

예를 들어 테스트하고 싶은 매우 특별한 상황이 있을 수 있습니다.

또는 자체 연산자를 구현하도록 선택하고 작동 방식을 테스트할 수 있습니다.

두 경우 모두  TestPublisher<T>사용하여 프로그래밍 방식으로 기타 신호를 트리거할 수 있습니다.

  • next(T value) 또는  next(T value, T rest) –  하나 이상의 신호를 구독자에게 보냅니다.
  • emit(T value) –  next(T)  와 동일 하지만  나중에 complete() 를 호출합니다  .
  • complete() – 완전한 신호  로 소스를 종료합니다. 
  • error(Throwable tr) –  오류가 있는 소스를 종료합니다.
  • flux() –  TestPublisher Flux 로 래핑하는 편리한 방법
  • mono() flux()  와 동일 하지만 Mono 로 랩핑됩니다.

4.1. TestPublisher 만들기

몇 가지 신호를 보낸 다음 예외와 함께 종료되는 간단한 TestPublisher  를 만들어 보겠습니다.

TestPublisher
  .<String>create()
  .next("First", "Second", "Third")
  .error(new RuntimeException("Message"));

4.2. 작동 중인 TestPublisher

앞에서 언급했듯이 때때로 특정 상황에 근접하게 일치하는 정교하게 선택된 신호를 트리거해야 할 수 있습니다.

이제 이 경우 데이터 소스를 완전히 마스터하는 것이 특히 중요합니다. 이를 달성하기 위해 다시  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  가 복잡한 논리와 연산자가 있는 클래스이고  원본  게시자 로부터 매우 특정한 데이터를 제공해야 한다고 가정합니다 .

TestPublisher 를 사용하면 쉽게 달성할 수 있습니다  .

final 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  게시자를 만듭니다. 그런 다음 TestPublisher 는 세 가지 요소를 방출하고 완료합니다.

4.3. 오작동 하는 TestPublisher 

반면에 createNonCompliant 팩터리 메서드 를 사용하여 오작동하는 TestPublisher만들 수 있습니다 . TestPublisher.Violation 에서 하나의 enum 값을 생성자에 전달해야 합니다  .  이러한 값은 게시자가 간과할 수 있는 사양 부분을 지정합니다.

null 요소 에 대해 NullPointerException  을 발생 시키지 않는 TestPublisher  를 살펴보겠습니다 .

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

ALLOW_NULL 외에도 TestPublisher.Violation  을  사용  하여 다음을 수행할 수 있습니다.

  • REQUEST_OVERFLOW  – 요청 수가 충분하지 않은 경우 IllegalStateException 을 발생 시키지 않고 next()  를 호출할 수 있습니다.
  • CLEANUP_ON_TERMINATE –  종료 신호를 연속으로 여러 번 보낼 수 있습니다.
  • DEFER_CANCELLATION –  취소 신호를 무시하고 요소 방출을 계속할 수 있습니다.

5. 결론

이 기사에서는 Spring Reactor 프로젝트 에서 반응형 스트림을 테스트하는 다양한 방법에 대해 논의  했습니다.

먼저 StepVerifier 를 사용하여 게시자를 테스트하는 방법을 살펴보았습니다. 그런 다음 TestPublisher를 사용하는 방법을 살펴보았습니다. 유사하게, 우리는 오작동하는 TestPublisher 로 작동하는 방법을 보았습니다 .

평소와 같이 모든 예제의 구현은 Github 프로젝트 에서 찾을 수 있습니다 .

Junit footer banner