[Spring-Reactive] Reactor (Testing)
in Spring on Spring
- StepVerifier를 이용한 Testing 1
- StepVerifier를 이용한 Testing 2
- TestPublisher를 이용한 Testing
- PublisherProbe를 이용한 Testing
StepVerifier를 이용한 Testing 1
- Flux 또는 Mono로 선언된 Operator 체인을 구독 시, 동작 방식을 테스트하기 위한 가장 일반적인 테스트 방식
- expectXXXX()를 이용해서 Sequence 상에서 예상되는 Signal의 기대값을 assertion할 수 있다.
- verify() Operator를 호출함으로써 전체 Operator 체인의 테스트를 트리거 한다.
- verifyXXXX()를 이용해서 전체 Operator 체인의 테스트를 트리거 + 종료 또는 에러 이벤트 검증을 수행할 수 있다.
- 실제 수행되는 시간과 관련된 테스트를 수행할 수 있다.
GeneralExample Class
- 테스트시 필요한 클래스
import reactor.core.publisher.Flux;
public class GeneralExample {
public static Flux<String> sayHelloReactor() {
return Flux
.just("Hello", "Reactor");
}
public static Flux<Integer> divideByTwo(Flux<Integer> source) {
return source
.zipWith(Flux.just(2, 2, 2, 2, 2), (x, y) -> x/y);
}
public static Flux<Integer> occurError(Flux<Integer> source) {
return source
.zipWith(Flux.just(2, 2, 2, 2, 0), (x, y) -> x/y);
}
public static Flux<Integer> takeNumber(Flux<Integer> source, long n) {
return source
.take(n);
}
}
TimeBasedExample Class
- 테스트시 필요한 클래스
public class TimeBasedExample {
public static Flux<Tuple2<String, Integer>> getCOVID19Count(Flux<Long> source) {
return source
.flatMap(notUse -> Flux.just(
Tuples.of("서울", 1000),
Tuples.of("경기도", 500),
Tuples.of("강원도", 300),
Tuples.of("충청도", 60),
Tuples.of("경상도", 100),
Tuples.of("전라도", 80),
Tuples.of("인천", 200),
Tuples.of("대전", 50),
Tuples.of("대구", 60),
Tuples.of("부산", 30),
Tuples.of("제주도", 5)
)
);
}
public static Flux<Tuple2<String, Integer>> getVoteCount(Flux<Long> source) {
return source
.zipWith(Flux.just(
Tuples.of("중구", 15400),
Tuples.of("서초구", 20020),
Tuples.of("강서구", 32040),
Tuples.of("강동구", 14506),
Tuples.of("서대문구", 35650)
)
)
.map(Tuple2::getT2);
}
}
Test
- 테스트 directory
import com.itvillage.section10.class00.GeneralExample;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
/***
* expectNext()를 사용하여 emit 된 n 개의 데이터를 검증하는 예제
* 결과는 성공
*/
public class StepVerifierGeneralTestExample02 {
@Test
public void sayHelloReactorTest() {
StepVerifier
.create(GeneralExample.sayHelloReactor())
.expectSubscription()
.expectNext("Hello")
.expectNext("Reactor")
.expectComplete()
.verify();
}
}
import com.itvillage.section10.class00.GeneralExample;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
/**
* - verifyComplete()을 사용하여 검증 실행 및 기대값으로 onComplete signal 이 맞는지 검증하는 예제
* - as(description)를 사용해서 실패한 expectXXXX()에게 description 을 지정할 수 있다.
* 결과 : Hi에서 실패를 하게 되며 '# expect Hi' 의 값이 출력된다.
*/
public class StepVerifierGeneralTestExample03 {
@Test
public void sayHelloReactorTest() {
StepVerifier
.create(GeneralExample.sayHelloReactor())
.expectSubscription()
.as("# expect subscription")
.expectNext("Hi")
.as("# expect Hi")
.expectNext("Reactor")
.as("# expect Reactor")
.verifyComplete();
}
}
import com.itvillage.section10.class00.GeneralExample;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
/**
* onError signal 발생 여부를 검증
* 결과 : 10을 0으로 나눌 수 업기 때문에 에러가 나며, expectError 에서 검증되며 성공 리턴.
*/
public class StepVerifierGeneralTestExample04 {
@Test
public void occurErrorTest() {
Flux<Integer> source = Flux.just(2, 4, 6, 8, 10);
StepVerifier
.create(GeneralExample.occurError(source))
.expectSubscription()
.expectNext(1)
.expectNext(2)
.expectNext(3)
.expectNext(4)
.expectError()
.verify();
}
}
import com.itvillage.section10.class00.GeneralExample;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
/**
* 1개 이상의 emit 된 데이터를 한꺼번에 검증
* 결과 : 성공
*/
public class StepVerifierGeneralTestExample05 {
@Test
public void divideByTwoTest() {
Flux<Integer> source = Flux.just(2, 4, 6, 8, 10);
StepVerifier
.create(GeneralExample.divideByTwo(source))
.expectSubscription()
.expectNext(1, 2, 3, 4, 5)
.expectComplete()
.verify();
}
}
import com.itvillage.section10.class00.GeneralExample;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import reactor.test.StepVerifierOptions;
/**
* onNext signal 을 통해 emit 된 데이터의 개수를 검증하는 예제
* - 검증에 실패할 경우에는 StepVerifierOptions에서 지정한 Scenario Name이 표시된다.
*/
public class StepVerifierGeneralTestExample06 {
@Test
public void rangeNumberTest() {
Flux<Integer> source = Flux.range(0, 1000);
StepVerifier
.create(GeneralExample.takeNumber(source, 500),
StepVerifierOptions.create().scenarioName("Verify from 0 to 499"))
.expectSubscription()
.expectNext(0)
.expectNextCount(498)
.expectNext(499)
.expectComplete()
.verify();
}
}
import com.itvillage.section10.class00.TimeBasedExample;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import reactor.test.scheduler.VirtualTimeScheduler;
import java.time.Duration;
/**
* 실제 시간을 가상 시간으로 대체하는 테스트 예제
* - 특정 시간만큼 시간을 앞당긴다.
* 참고 : 여기서 take(1) 는 interval을 1번만 한다는 뜻, 예제 기준으로 12시간 후에 이벤트 한번만 실행, 이후엔 실행 안한다.
*/
public class StepVerifierTimeBasedTestExample01 {
@Test
public void getCOVID19CountTest() {
StepVerifier
.withVirtualTime(() -> TimeBasedExample.getCOVID19Count(
Flux.interval(Duration.ofHours(12)).take(1)
)
)
.expectSubscription()
.then(() -> VirtualTimeScheduler.get().advanceTimeBy(Duration.ofHours(12)))
.expectNextCount(11)
.expectComplete()
.verify();
}
}
/**
* 실제 시간을 가상 시간으로 대체하는 테스트 예제
* - thenAwait(Duration)을 통해 특정 시간만큼 가상으로 기다린다.
* - 즉, 특정 시간을 기다린 것처럼 시간을 당긴다.
*/
public class StepVerifierTimeBasedTestExample02 {
@Test
public void getCOVID19CountTest() {
StepVerifier
.withVirtualTime(() -> TimeBasedExample.getCOVID19Count(
Flux.interval(Duration.ofHours(12)).take(1)
)
)
.expectSubscription()
.thenAwait(Duration.ofHours(12))
.expectNextCount(11)
.expectComplete()
.verify();
}
}
import com.itvillage.section10.class00.TimeBasedExample;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import java.time.Duration;
/**
* 검증에 소요되는 시간을 제한하는 예제
* - verify(Duration)을 통해 설정한 시간내에 검증이 끝나는지를 확인할 수 있다.
* - verify(Duration)에 설정한 시간까지 실행한다.
*/
public class StepVerifierTimeBasedTestExample03 {
@Test
public void getCOVID19CountTest() {
StepVerifier
.create(TimeBasedExample.getCOVID19Count(
Flux.interval(Duration.ofMinutes(1)).take(1)
)
)
.expectSubscription()
.expectNextCount(11)
.expectComplete()
.verify(Duration.ofSeconds(3));
}
}
/**
* expectNoEvent(Duration)으로 지정된 대기 시간동안 이벤트가 없을을 확인하는 예제
*/
public class StepVerifierTimeBasedTestExample04 {
@Test
public void getCOVID19CountTest() {
StepVerifier
.withVirtualTime(() -> TimeBasedExample.getVoteCount(
Flux.interval(Duration.ofMinutes(1))
)
)
.expectSubscription()
.expectNoEvent(Duration.ofMinutes(1))
.expectNext(Tuples.of("중구", 15400))
.expectNoEvent(Duration.ofMinutes(1))
.expectNoEvent(Duration.ofMinutes(1))
.expectNoEvent(Duration.ofMinutes(1))
.expectNoEvent(Duration.ofMinutes(1))
.expectNextCount(4)
.expectComplete()
.verify();
}
}
StepVerifier를 이용한 Testing 2
- Backpressure 테스트
- hasDropped(), hasDiscarded() 등을 이용해서 backpressure 테스트를 수행할 수 있다.
- Context 테스트
- expectAccessibleContext()를 이용해서 접근 가능한 Context가 있는지 테스트 할 수 있다.
- hasKey()를 사용하여 Context의 key가 존재하는지 검증할 수 있다.
- 기록된 데이터를 이용한 테스트
- recordWith()를 사용하여 emit 된 데이터를 기록할 수 있다.
- consumeRecordedWith()를 사용하여 기록된 데이터들을 소비하며 검증할 수 있다.
- expectRecordedMatches()를 사용하여 기록된 데이터의 컬렉션을 검증할 수 있다.
BackpressureExample
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
public class BackpressureExample {
public static Flux<Integer> generateNumberByErrorStrategy() {
return Flux
.create(emitter -> {
for (int i = 1; i <= 100; i++) {
emitter.next(i);
}
emitter.complete();
}, FluxSink.OverflowStrategy.ERROR);
}
public static Flux<Integer> generateNumberByDropStrategy() {
return Flux
.create(emitter -> {
for (int i = 1; i <= 100; i++) {
emitter.next(i);
}
emitter.complete();
}, FluxSink.OverflowStrategy.DROP);
}
}
ContextExample
import reactor.core.publisher.Mono;
public class ContextExample {
public static Mono<String> helloMessage(Mono<String> source, String key) {
return source
.zipWith(Mono.deferContextual(ctx -> Mono.just(ctx.get(key)))) // 두 개의 Mono 스트림을 결합하여 **튜플(Tuple2)**로 반환
.map(tuple -> tuple.getT1() + ", " + tuple.getT2());
}
}
RecordExample
import reactor.core.publisher.Flux;
public class RecordExample {
public static Flux<String> getCountry(Flux<String> source) {
return source
.map(country -> country.substring(0, 1).toUpperCase() + country.substring(1));
}
}
Test
BackpressureTest
- Discarded: 스트림 완료 또는 종료 시, 버퍼에 있던 데이터가 의도적으로 버려진 경우.
- Dropped: 백프레셔나 오버플로우로 인해 처리되지 않고 무시된 데이터.
import com.itvillage.section10.class01.BackpressureExample;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
/**
* Backpressure 전략에 따라 Exception이 발생하는 예제
* - request 데이터 개수보다 많은 데이터가 emit 되어 OverFlowException이 발생
* - OverFlowException이 발생하게 된 데이터는 discard 된다.
* - 나머지 emit 된 데이터들은 Hooks.onNextDropped()에 의해 drop된다.
* - StepVerifier.create(Flux, 1L)는 구독 시 최대 1개만 요청되야 하지만 100개가 요청되어 실패 된다.
* - thenConsumeWhile 는 소비되는 데이터가 주어진 조건에 맞아야 성공.
*/
public class StepVerifierBackpressureTestExample01 {
@Test
public void generateNumberTest() {
StepVerifier
.create(BackpressureExample.generateNumberByErrorStrategy(), 1L)
.thenConsumeWhile(num -> num >= 1) // emit 된 데이터들을 소비한다.
.verifyComplete();
}
}
import com.itvillage.section10.class01.BackpressureExample;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
/**
* Backpressure ERROR 전략을 검증하는 예제
* - expectError()를 사용하여 에러가 발생되었는지 검증
* - verifyThenAssertThat()을 사용하여 검증 이후에 assertion method 를 사용하여 추가 검증을 할 수 있다.
* - hasDiscardedElements()를 사용하여 discard된 데이터가 있는지 검증한다. OverflowException이 발생할 때 2가 discard된다.
* - hasDiscarded()를 사용하여 discard된 데이터가 무엇인지 검증한다. OverflowException이 발생할 때 2가 discard된다.
* - hasDroppedElements()을 사용하여 Hooks.onNextDropped()으로 Drop된 데이터가 있는지를 검증한다.
* - hasDropped()를 사용하여 Hooks.onNextDropped()으로 Drop된 데이터가 무엇인지 검증한다.
* - hasDiscarded : 버퍼에 잠깐 들어갔지만, 요청이 없어서 사용되지 않고 버려진 데이터(2)
* - hasDropped : 버퍼에 들어갈 자리조차 없어서 처음부터 무시된 데이터(3, 4, 5, ...).
*/
public class StepVerifierBackpressureTestExample02 {
@Test
public void generateNumberTest() {
StepVerifier
.create(BackpressureExample.generateNumberByErrorStrategy(), 1L)
.thenConsumeWhile(num -> num >= 1)
.expectError()
.verifyThenAssertThat()
.hasDiscardedElements()
.hasDiscarded(2)
.hasDroppedElements()
.hasDropped(3, 4, 5, 6, 98, 99, 100);
}
}
import com.itvillage.section10.class01.BackpressureExample;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
/**
* Backpressure DROP 전략을 검증하는 예제
* - expectError()를 사용하여 에러가 발생되었는지 검증
* - verifyThenAssertThat()을 사용하여 검증 이후에 assertion method 를 사용하여 추가 검증을 할 수 있다.
* - hasDiscardedElements()를 사용하여 discard된 데이터가 있는지를 검증한다. Backpressure DROP 전략은 Drop된 데이터가 discard된다.
* - hasDiscarded()를 사용하여 discard된 데이터가 무엇인지 검증한다. Backpressure DROP 전략은 Drop된 데이터가 discard된다.
*/
public class StepVerifierBackpressureTestExample03 {
@Test
public void generateNumberTest() {
StepVerifier
.create(BackpressureExample.generateNumberByDropStrategy(), 1L)
.thenConsumeWhile(num -> num >= 1)
.expectComplete()
.verifyThenAssertThat()
.hasDiscardedElements()
.hasDiscarded(2, 3, 4, 5, 6, 98, 99, 100);
// .hasDropped(2, 3, 4, 5, 6, 98, 99, 100);
}
}
RecordTest
import com.itvillage.section10.class01.RecordExample;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import java.util.ArrayList;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasLength;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.core.Every.everyItem;
/**
* emit 되는 모든 데이터들을 캡쳐하여 컬렉션에 기록한 후, 기록된 데이터들을 검증하는 예제
* - recordWith()를 사용하여 emit 된 데이터를 기록하는 세션을 시작한다.
* - thenConsumeWhile()을 사용하여 조건에 맞는 데이터만 소비한다. 여기서 조건에 맞는 데이터들이 ArrayList 에 추가(기록)된다.
* - consumeRecordedWith()를 사용하여 기록된 데이터들을 소비한다. 여기서는 assertThat()을 사용하여 검증한다.
*/
public class StepVerifierRecordTestExample01 {
@Test
public void getCountryTest() {
StepVerifier
.create(RecordExample.getCountry(Flux.just("france", "russia", "greece", "poland")))
.expectSubscription()
.recordWith(ArrayList::new) // 소비된 데이터를 기록할 컬렉션(ArrayList) 준비
.thenConsumeWhile(country -> !country.isEmpty()) // country 스트림을 소비, 빈 문자열이 나오면 중단
// 기록된 데이터(countries)에 대한 추가 검증
.consumeRecordedWith(countries -> {
assertThat(countries, everyItem(hasLength(6)));
assertThat(
countries
.stream()
.allMatch(country -> Character.isUpperCase(country.charAt(0))),
is(true)
);
})
.expectComplete()
.verify();
}
}
import com.itvillage.section10.class01.RecordExample;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import java.util.ArrayList;
/**
* emit 되는 모든 데이터들을 캡쳐하여 컬렉션에 기록한 후, 기록된 데이터들을 검증하는 예제
* - recordWith()를 사용하여 emit 된 데이터를 기록하는 세션을 시작한다.
* - thenConsumeWhile()을 사용하여 조건에 맞는 데이터만 소비한다. 여기서 조건에 맞는 데이터들이 ArrayList 에 추가(기록)된다.
* - expectRecordedMatches()를 사용하여 기록된 데이터의 컬렉션을 검증한다.
*/
public class StepVerifierRecordTestExample02 {
@Test
public void getCountryTest() {
StepVerifier
.create(RecordExample.getCountry(Flux.just("france", "russia", "greece", "poland")))
.expectSubscription()
.recordWith(ArrayList::new)
.thenConsumeWhile(country -> !country.isEmpty())
.expectRecordedMatches(countries ->
countries
.stream()
.allMatch(country ->
Character.isUpperCase(country.charAt(0))))
.expectComplete()
.verify();
}
}
ContextTest
import com.itvillage.section10.class01.ContextExample;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
/**
* Reactor Sequence 에서 사용되는 Context 를 검증하는 예제
* - expectAccessibleContext()을 사용하여 접근 가능한 Context가 있는지를 검증한다.
* - hasKey()를 사용하여 Context의 key가 존재하는지 검증한다.
* - then()을 사용하여 검증을 위한 후속 작업을 진행할 수 있다.
*/
public class StepVerifierContextTestExample01 {
final private static String KEY = "helloTarget";
@Test
public void helloMessageTest() {
Mono<String> source = Mono.just("Hello");
StepVerifier
.create(ContextExample
.helloMessage(source, KEY)
.contextWrite(context -> context.put(KEY, "Reactor"))
)
.expectSubscription()
.expectAccessibleContext()
.hasKey("helloTarget")
.then()
.expectNext("Hello, Reactor")
.expectComplete()
.verify();
}
}
TestPublisher를 이용한 Testing
- Testing 목적에 사용하기 위한 Publisher이다.
- 개발자가 직접 프로그래밍을 통해 signal을 발생시킬 수 있다.
- 주로 특정한 상황을 재현하여 테스트하고 싶은 경우 사용할 수 있다.
- 리액티브 스트림즈 사양을 준수하는지의 여부를 테스트할 수 있다.
import com.itvillage.section10.class00.GeneralExample;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
import reactor.test.publisher.TestPublisher;
/**
* TestPublisher 를 사용해서 서비스 로직의 메서드에 대한 Unit Test 를 실시하는 예제
* - 정상 동작하는 TestPublisher
* - next() 사용
*/
public class TestPublisherTestExample01 {
@Test
public void divideByTwoTest() {
TestPublisher<Integer> source = TestPublisher.create();
StepVerifier
.create(GeneralExample.divideByTwo(source.flux())) // 테스트퍼블리셔를 받고 있다.
.expectSubscription()
.then(() -> source.next(2, 4, 6, 8, 10)) // 데이터 emit
.expectNext(1, 2, 3, 4, 5)
.expectComplete()
.verify();
}
}
import com.itvillage.section10.class00.GeneralExample;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
import reactor.test.publisher.TestPublisher;
/**
* TestPublisher 를 사용해서 서비스 로직의 메서드에 대한 Unit Test 를 실시하는 예제
* - 정상 동작하는 TestPublisher
* - next() 사용
* - 에러 발생 여부 검증
*/
public class TestPublisherTestExample02 {
@Test
public void divideByTwoTest() {
TestPublisher<Integer> source = TestPublisher.create();
StepVerifier
.create(GeneralExample.occurError(source.flux()))
.expectSubscription()
.then(() -> source.next(2, 4, 6, 8, 10))
.expectNext(1)
.expectNext(2)
.expectNext(3)
.expectNext(4)
.expectError()
.verify();
}
}
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
import reactor.test.publisher.TestPublisher;
/**
* TestPublisher 를 사용해서 서비스 로직의 메서드에 대한 Unit Test 를 실시하는 예제
* - 정상 동작하는 TestPublisher
* - emit() 사용
*/
public class TestPublisherTestExample03 {
@Test
public void divideByTwoTest() {
TestPublisher<Integer> source = TestPublisher.create();
StepVerifier
.create(source.flux())
.expectSubscription()
.then(() -> source.emit(1, 2, 3))
.expectNext(1)
.expectNext(2)
.expectNext(3)
.expectComplete()
.verify();
}
}
import com.itvillage.section10.class00.GeneralExample;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
import reactor.test.publisher.TestPublisher;
/**
* TestPublisher 를 사용해서 서비스 로직의 메서드에 대한 Unit Test 를 실시하는 예제
* - Reactive Streams 사양을 위반해도 Publisher가 정상 동작하게 함으로써 서비스 로직을 검증하는 예제
* - 에러의 내용이 다르다. (null 입력이 안됨 vs null 입력은 되지만 이후 에러)
*/
public class TestPublisherTestExample04 {
@Test
public void divideByTwoTest() {
TestPublisher<Integer> source = TestPublisher.createNoncompliant(TestPublisher.Violation.ALLOW_NULL); // 사양을 준수 하지 않는 방법
// TestPublisher<Integer> source = TestPublisher.create();
StepVerifier
.create(GeneralExample.divideByTwo(source.flux()))
.expectSubscription()
.then(() -> source.next(2, 4, 6, 8, null))
.expectNext(1)
.expectNext(2)
.expectNext(3)
.expectNext(4)
.expectComplete()
.verify();
}
}
PublisherProbe를 이용한 Testing
- Operator 체인의 실행 경로를 검증할 수 있다.
- 주로 조건에 따른 분기로 인해서 Sequence가 분기 되는 경우, 실행 경로를 추적해서 정상적으로 실행이 되었는지 확인할 수 있다.
- 해당 실행 경로대로 정상적으로 실행되었는지의 여부는 assertWasSubscribed(), assertWasRequested(), assertWasCancelled() 를 통해 검증할 수 있다.
class
package com.itvillage.section10.class02;
import reactor.core.publisher.Mono;
public class PublisherProbeExample {
public static Mono<String> processWith(Mono<String> main, Mono<String> standby) {
return main
.flatMap(massage -> Mono.just(massage))
.switchIfEmpty(standby);
}
public static Mono<String> useMainPower() {
return Mono.empty();
}
public static Mono useStandbyPower() {
return Mono.just("# use Standby Power");
}
}
Test
import com.itvillage.section10.class02.PublisherProbeExample;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
import reactor.test.publisher.PublisherProbe;
public class PublisherProbeTestExample01 {
@Test
public void publisherProbeTest() {
PublisherProbe<String> probe = PublisherProbe.of(PublisherProbeExample.useStandbyPower());
StepVerifier
.create(PublisherProbeExample.processWith(PublisherProbeExample.useMainPower(), probe.mono()))
.expectNextCount(1)
.verifyComplete();
// 실행 경로 대로 실행 되었는지 확인(순서는 상관 없음)
probe.assertWasSubscribed(); // 구독 확인
probe.assertWasRequested(); // 데이터 요청 확인
probe.assertWasNotCancelled(); // 구독 취소가 없었는지 확인
}
}