서론
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가 동작
참고
- 스프링으로 시작하는 리액티브 프로그래밍 (황정식 저자)
'Spring > 스프링으로 시작하는 리액티브 프로그래밍' 카테고리의 다른 글
[Reactor] 자주 사용되는 Operator (0) | 2024.08.11 |
---|---|
[Spring] Spring WebFlux 개요 (0) | 2024.08.06 |
[Reactor] Reactor Sequence 디버깅 방법 (0) | 2024.07.31 |
[Reactor] Context (0) | 2024.07.30 |
[Reactor] Scheduler (0) | 2024.07.24 |