1. 개요
이 사용방법(예제)에서는 StepVerifier 및 TestPublisher 를 사용하여 반응형 스트림 을 테스트하는 방법을 자세히 살펴 보겠습니다 .
일련의 리액터 작업을 포함하는 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 프로젝트 에서 찾을 수 있습니다 .