Spring/스프링으로 시작하는 리액티브 프로그래밍

[Reactor] Reactor 테스트

꾸준함. 2024. 7. 31. 19:32

서론

Reactor에서는 reactor-test라는 테스트 전용 모듈을 통해 여러 가지 유형의 테스트를 지원합니다.

reactor-test 모듈의 기능을 사용하기 위해서는 build.gradle 파일의 dependencies에 다음 의존성을 추가해야 합니다.

 

testImplementation 'io.projectreactor:reactor-test'

 

1. StepVerifier를 사용한 테스트

Reactor에서 가장 일반적인 테스트 방식은 Flux 혹은 Mono를 Reactor Sequence로 정의한 후 구독 시점에 해당 Operator 체인이 시나리오대로 동작하는지를 테스트하는 방식이며 이를 위해 StepVerifier라는 API를 제공합니다.

  • ex) Reactor Sequence에서 다음에 발생할 시그널이 무엇인지, 기대했던 데이터들이 Publisher로부터 내보내졌는지, 특정 시간 동안 emit 된 데이터가 있는지 등을 단계적으로 테스트

 

1.1 Signal 이벤트 테스트

  • StepVerifier를 이용한 가장 기본 테스트 방식은 Reactor Sequence에서 발생하는 Signal 이벤트를 테스트하는 것

 

1.1.1 StepVerifier API를 이용한 Operator 체인의 기본적인 테스트 방법

  • create() 메서드를 통해 테스트 대상 Sequence 생성
  • expectXXXX()를 통해 Sequence에서 예상되는 Signal의 기댓값을 평가
  • verify()를 호출함으로써 전체 Operator 체인의 테스트를 trigger

 

1.1.2 expectXXXX() 메서드

 

메서드 설명
expectSubscription() 구독이 이루어짐을 기대
expectNext(T t) onNext Signal을 통해 전달되는 값이 파라미터로 전달된 값과 같음을 기대
expectComplete() onComplete Signal이 전송되기를 기대
expectError() onError Signal이 전송되기를 기대
expectNextCount(long count) 구독 시점 또는 이전 expectNext()를 통해 기댓값이 평가된 데이터 이후부터 emit된 수를 기대
expectNoEvent(Duration duration) 주어진 시간 동안 Signal 이벤트가 발생하지 않았음을 기대
expectAccessibleContext() 구독 시점 이후에 Context가 전파되었음을 기대
expectNextSequence(Iterable<? extends T> iterable) emit된 데이터들이 파라미터로 전달된 Iterable의 요소와 매치됨을 기대

 

1.1.3 verifyXXX() 메서드

 

메서드 설명
verify() 검증을 trigger
verifyComplete() 검증을 trigger하고 onComplete Signal 기대
verifyError() 검증을 trigger하고 onError Signal 기대
verifyTimeout(Duration duration) 검증을 trigger하고 주어진 시간이 초과되어도 Publisher가 종료되지 않음을 기대

 

1.1.4.1 Signal 이벤트 테스트 예제 #1

 

 

 

부연 설명

  • as() 메서드를 사요앻서 이전 기댓값 평가 단계에 대한 description 추가 가능
    • 만약 테스트에 실패하게 되면 실패한 단계에 해당하는 설명이 로그로 출력
    • 위 예제에서 "# expect Hi"가 찍힌 것을 확인할 수 있음 (실제 emit 된 값은 Hello)

 

1.1.4.2 Signal 이벤트 테스트 예제 #2

 

 

 

부연 설명

  • emit된 네 개의 데이터는 expectNext()를 통해 기댓값 평가에 성공하지만 마지막에 emit 된 데이터는 2가 아닌 0으로 나누는 작업을 했기 때문에 ArithmeticException 발생
    • expectError()를 통해 에러를 기대했기 때문에 최종 테스트 결과는 pass

 

1.1.4.3 Signal 이벤트 테스트 예제 #3

 

 

부연 설명

  • takeNumber() 메서드는 Source Flux에서 파라미터로 전달된 숫자의 개수만큼만 데이터를 내보내는 메서드
  • 테스트 대상 Flux가 총 500개의 숫자를 내보내는데 첫 번째 emit된 숫자 0을 평가한 후 expectNextCount()를 통해 그다음부터 emit 된 데이터의 개수가 498개라고 기대
    • 즉, 내부적으로 498개의 숫자가 emit된다는 의미이므로 expectNextCount(498)까지 emit 된 데이터의 개수는 499
    • 테스트 대상 Flux가 총 500개의 숫자이고 0부터 시작했기 때문에 마지막으로 평가해야 되는 기댓값은 500이 아닌 499이기 때문에 테스트 실패 발생

 

1.2 Time-based 테스트

  • StepVerifier는 가상의 시간을 이용해 미래에 실행되는 Reactor Sequence의 시간을 앞당겨 테스트할 수 있는 기능을 지원

 

1.2.1 Time-based 테스트 예제 #1

  • 테스트의 목적은 현재 시점에서 1시간 뒤에 코로나 확진자 발생 현황을 체크하는 것

 

 

부연 설명

  • 원래대로라면 Sequence가 동작하기까지 실제로 1시간을 기다려야 알 수 있기 때문에 매우 비효율적인 테스트
  • Reactor에서는 이러한 케이스를 고려하여 withVirtualTime() 메서드를 제공하여 VirtualTimeScheduler라는 가상 스케줄러의 제어를 받도록 제어
    • 구독에 대한 기댓값을 평가하고 난 후 then() 메서드를 사용해서 후속 작업을 할 수 있도록 하는데 여기서는 VirtualTimeScheduler의 advanceTimeBy()를 이용해 시간을 1시간 당기는 작업 수행

 

1.2.2 Time-based 테스트 예제 #2

  • 테스트 대상 메서드에 대한 기댓값을 평가하는데 걸리는 시간을 제한하는 예제

 

 

부연 설명

  • 3초 내에 기댓값의 평가가 끝나지 않으면 시간 초과로 간주하는 테스트
  • 결과적으로 기댓값의 평가가 3초 이내에 이루어지지 않았기 때문에 AssertionError 발생

 

1.2.3 Time-based 테스트 예제 #3

  • expectNoEvent()를 사용해서 지정한 시간 동안 어떤 Signal 이벤트도 발생하지 않았음을 기대하는 예제

 

 

부연 설명

  • expectNoEvent() 메서드를 통해 1분 동안 onNext Signal 이벤트가 발생하지 않을 것이라고 기대
    • expectNoEvent()의 파라미터로 시간을 지정하면 지정한 시간 동안 어떤 이벤트도 발생하지 않을 것이라고 기대하는 동시에 지정한 시간만큼 시간을 앞당김

 

1.3 Backpressure 테스트

  • StepVerifier를 사용하면 Backpressure에 대한 테스트 수행 가능

 

1.3.1 Backpressure 테스트 예제 #1

 

 

 

부연 설명

  • generateNumber() 메서드는 한 번에 100개의 숫자 데이터를 내보내는데 StepVerifier의 create() 메서드에서 데이터의 요청 개수를 1로 지정해서 overflow가 발생했기 때문에 테스트 결과는 실패
    • thenConsumeWhile() 메서드를 사용하여 emit 되는 데이터를 소비하고 있지만 예상한 것보다 더 많은 데이터를 수신함에 따라 overflow 발생
    • 기대했던 onComplete() 시그널이 아닌 onError 시그널 발생

 

1.3.2 Backpressure 테스트 예제 #2

 

 

부연 설명

  • exprectError()를 통해 에러를 기대했고 overflow로 인해 내부적으로 Drop 된 데이터가 있음을 Assertion
  • verifyThenAssertThat() 메서드를 사용할 경우 검증을 trigger하고 난 후 추가적인 Assertion을 할 수 있음
    • hasDroppedElements() 메서드를 이용해 Drop된 데이터가 있음을 검증

 

1.4 Context 테스트

  • Reactor Sequence에서 사용되는 Context 역시 StepVerifier를 사용해서 테스트 가능

 

1.4.1 Context 테스트 예제

 

 

부연 설명

  • Context에는 두 개의 데이터가 저장되어 있음
    • Base64 형식으로 인코딩 된 secret key
    • secret key에 해당하는 secret message

 

  • getSecretMessage() 메서드는 파라미터로 입력받은 Mono<String> keySource와 Context에 저장된 secret key의 값을 비교해서 일치하면 Context에 저장된 Mono<String> secretMessage를 반환
  • 테스트에서는 hasKey()로 전파된 Context에 "secretKey"와 "secretMessage" 키에 해당하는 값이 있음을 기대
  • then() 메서드로 Sequence의 다음 Signal 이벤트의 기댓값을 평가
  • expectNext()로 `Hello, Reactor` 문자열이 emit 되었음을 기대
  • expectComplte()로 onComplete 시그널이 전송됨을 기대

 

1.5 Record 기반 테스트

  • 데이터의 단순한 기댓값만 평가하는 것이 아니라 조금 더 구체적인 조건으로 Assertion해야 하는 경우 적용되는 테스트
  • recordWith()는 파라미터로 전달된 자바의 컬렉션에 emit된 데이터를 추가하는 세션을 시작

 

1.5.1 Record 기반 테스트 예제 #1

 

 

 

부연 설명

  • getCapitalizedCountry() 메서드가 알파벳으로 된 국가명을 전달받아서 첫 글자를 대문자로 변환하도록 정의된 Flux를 반환
  • recordWith()로 emit된 데이터에 대한 기록 시작
  • thenConsumeWhile()로 파라미터로 전달한 Predicate과 일치하는 데이터는 다음 단계에서 소비할 수 있도록 지원
  • consumeRecordedWith()로 컬렉션에 기록된 데이터를 소비하며 여기서는 모든 데이터의 첫 글자가 대문자인지 여부를 확인함으로써 getCapitalizedCountry() 메서드를 Assertion
  • expectComplete()으로 onComplete 시그널이 전송됨을 기대

 

2. TestPublisher를 사용한 테스트

reactor-test 모듈에서 지원하는 테스트 전용 Publisher인 TestPublisher를 이용해 테스트를 진행할 수 있습니다.

TestPublisher를 사용하면 개발자가 직접 프로그래밍 방식으로 Signal을 발생 시켜서 원하는 상황을 미세하게 재연하며 테스트를 진행할 수 있습니다.

 

2.1 정상 동작하는 TestPublisher

  • 정상 동작하는 TestPublisher라는 말의 의미는 emit하는emit 하는 데이터가 Null인지, 요청하는 개수보다 더 많은 데이터를 emit 하는지 등의 리액티브 스트림즈 사양 위반 여부를 사전에 체크한다는 의미
  • TestPublisher를 사용하면 다음 예제 코드처럼 숫자를 테스트하는 것이 아니라 복잡한 로직이 포함된 대상 메서드를 테스트하거나 조건에 따라 Signal을 변경해야 되는 등의 특정 상황을 테스트하기가 용이함

 

 

부연 설명

  • Testpublisher.create()으로 TestPublisher를 생성
  • 테스트 대상 클래스에 파라미터로 Flux를 전달하기 위해 flux() 메서드를 이용하여 Flux로 변환
  • 마지막으로 emit() 메서드를 사용해 테스트에 필요한 데이터를 emit

 

2.2 오동작하는 TestPublisher

  • 오동작하는 TestPublisher를 생성하여 리액티브 스트림즈의 사양을 위반하는 상황이 발생하는지를 테스트 가능
  • 오동작하는 TestPublisher를 생성하기 위한 Violation 조건
    • ALLOW_NULL: 전송할 데이터가 null이어도 NPE를 발생시키지 않고 다음 호출을 진행할 수 있도록 지원
    • CLEANUP_ON_TERMINATE: onComplete, onError, emit 같은 Terminal 시그널을 연달아 여러 번 보낼 수 있도록 지원
    • DEFER_CANCELLATION: cancel 시그널을 무시하고 계속해서 시그널을 emit할 수 있도록 지원
    • REQUEST_OVERFLOW: 요청 개수보다 더 많은 시그널이 발생하더라도 IllegalStateException을 발생시키지 않고 다음 호출을 진행할 수 있도록 지원

 

 

 

부연 설명

  • 오동작하는 TestPublisher로서 동작하도록 ALLOW_NULL 위반 조건을 지정하여 데이터의 값이 null이라도 정상 동작하는 TestPublisher를 생성하여 null 값을 포함하는 데이터 소스를 사용하도록 함
  • 실행 결과를 보면 TestPublisher가 onNext Signal을 전송하는 과정에서 NPE가 발생했다는 것을 알 수 있음

 

3. PublisherProbe를 사용한 테스트

reactor-test 모듈은 PublisherProbe를 이용해 Sequence의 실행 경로를 테스트할 수 있습니다.

주로 조건에 따라 Sequence가 분기되는 경우 Sequence의 실행 경로를 추적해서 정상적으로 실행되었는지 테스트할 수 있습니다.


 

부연 설명

  • 해당 코드의 주 목적은 Sequence의 Signal 이벤트뿐만 아니라 switchIfEmpty() Operator로 인해 Sequence가 분기되는 상황에서 실제로 어느 Publisher가 동작하는지 해당 Publisher의 실행 경로를 테스트하는 것
    • PublisherProbe 클래스의 assertWasSubscribed(), assertWasRequested(), assertWasNotCancelled() 메서드를 통해 기대하는 Publisher가 구독을 했는지, 요청을 했는지, 중간에 취소가 되지 않았는지를 Assertion 함으로써 Publisher의 실행 경로를 테스트
    • supplyMainPower() 메서드가 Mono.empty()를 반환하기 때문에 processTask() 메서드에서는 최종적으로 Mono<String> standby가 동작

 

참고

  • 스프링으로 시작하는 리액티브 프로그래밍 (황정식 저자)
반응형