일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- java #jvm #reference #gc #strong reference
- zuul
- spring cloud netflix zuul
- spring cloud netflix eureka
- spring cloud netflix
- docker
- Dynamic Routing
- unittest
- api-gateway
- test
- microservice architecture
- netflix eureka
- dfs
- BFS
- reactive
- springcloud
- Java
- forkandjoinpool #threadpool #jvm #async #non-blocking
- 탐색
- container image #docker #layer #filesystem #content addressable
- code refactoring
- Spring Data Redis
- Eureka
- spring cloud
- 설계
- netflix
- unit
- 단위테스트
- 서비스스펙
- Today
- Total
phantasmicmeans 기술 블로그
Reactor Test, thenConsumeWhile 연산자 사용시 주의할 점 본문
Reactor Test, thenConsumeWhile 연산자 사용시 주의할 점
phantasmicmeans 2021. 11. 12. 12:28먼저 expectNext / assertNext / consumeNextWith
(아래에서 설명할 thenConsumeWhile
와 비교를 위해 간단히 추가 했습니다.)
가장 기본적인 연산자로는 expectNext
/ assertNext
/ consumeNextWith
가 있습니다.
모두 결국 onNext signal
을 기반으로 동작합니다만, 이 연산자들은 아이템이 emit 되지 않으면 에러를 발생시킵니다.
즉, empty publisher를 차단할 수 있습니다.
(empty publisher 반환을 테스트하고 싶다면 아래와 같은 방법을 사용하거나 아래에서 설명할 thenConsumeWhile을 사용하면 됩니다.)
@Test
public void EMPTY_PUBLISHER_TEST() {
Mono<Integer> mono = doSomething();
StepVerifier.create(mono)
.expectSubscription()
.expectNextCount(0)
.verifyComplete();
}
thenConsumeWhile (with Flux)
thenConsumeWhile은 Predicate
가 참인 경우, 다음 onNext signal
을 consume하여 sequence 검증을 진행합니다. onNext
를 기준으로 반복 검증이 가능하다 보니 Flux sequence
를 검증할때 이 operator를 자주 활용합니다.
for문 돌면서 검증하는 것과 비슷하다고 볼 수 있기에, 간편하게 사용할 수 있으나 주의해야 할 점이 있습니다. 이 연산자는 onNext signal
을 consume 하여 시퀀스를 검증하지만, onNext signal
이 없는 경우 이를 skip 해버립니다.
따라서, 테스트 대상이 empty publisher를 반환하는 경우, 테스트 케이스는 성공하게 됩니다.
앞서 설명드린 `expectNext` / `assertNext` / `consumeNextWith`와 마찬가지로, 모두 `onNext signal`을 기반으로 동작합니다만, 이들은 아이템이 emit 되지 않으면 에러를 발생시킨다는 점에서 `thenConsumeWhile`과는 차이점을 가집니다.
간단히 정리하면 다음과 같습니다.
expectNext
/assertNext
/consumeNextWith
- Empty Publisher 차단 가능 (empty인 경우 테스트 실패)
thenConsumeWhile
- Empty Publisher 차단 불가능 (empty인 경우 skip)
기본적으로 Mono/Flux는 empty(0개) publisher를 지원합니다. (엄밀히는 subscriber의 onNext를 트리거 하지 않는 publisher)
- Mono: 0~1개 아이템
- Flux: 0~N개 아이템
그렇기에 thenConsumeWhile
연산자가 empty publisher 검증 시 skip 하는 행위가 당연하다고 볼수도 있습니다. 항상 next item이 있어야 하는건 아니니까요
예시
테스트 대상 코드에 switchIfEmpty
와 같은 operator를 사용하여 방지할 수는 있습니다. 그러나 빼먹는 케이스도 있을뿐더러 의도된 empty를 반환하는 경우도 있습니다.
스펙 자체가 empty publisher를 허용하기에 이를 원천 차단할 수는 없습니다.
그러나 thenConsumeWhile
기반 테스트 케이스를 작성할 때만큼은 empty publisher를 신경 써야 하는데요, 아래 예시로 설명드리겠습니다.
테스트 대상
// 특정 콘텐츠의 이미지 리스트를 찾아 리턴하는 Publisher
public Flux<Image> getImages(String id) {
return contentsRepository.findById(id) // 없으면 empty 리턴
.flatMapMany(content -> {
if (CollectionUtils.isEmpty(content.getImages())) {
return Flux.empty(); // empty 리턴
}
return Flux.fromIterable(content.getImages());
});
}
테스트 케이스
@Test
public void 컨텐츠_이미지_추출_테스트() {
String namePrefix = "file";
String urlPrefix = "https://test.test";
String id = "test_id";
// create images
List<Image> images = new ArrayList<>();
for (int i = 0; i < 3; i++) {
images.add(Image.builder()
.fileName(namePrefix + i)
.url(urlPrefix + i)
.build());
}
// insert mock content
contentsRepository.insert(Content.builder()
._id(id)
.images(images)
.build()
).block();
// verify
StepVerifier.create(getImages(id))
.thenConsumeWhile(image -> {
System.out.println("thenConsumeWhile");
return image.getFileName().startsWith(namePrefix) &&
image.getUrl().startsWith(urlPrefix);
}).verifyComplete();
}
아이디를 기반으로 해당 컨텐츠의 이미지 리스트 추출을 검증하는 케이스입니다. mock 컨텐츠도 제대로 insert 되었기에 empty publisher를 반환하지 않게 되구요. 따라서 thenConsumeWhile
도 의도대로 동작합니다.
테스트는 성공하고 아래처럼 로깅도 3번 진행됩니다.
새 아이디를 사용하여 테스트
이 상황에서 새로운 id를 사용하여 테스트해보겠습니다. ContentsRepository
에는 해당 아이디 기반의 컨텐츠가 없을 테니 당연히 테스트가 깨져야 합니다.
위 코드에서 id 파라미터만 test_id_new
로 변경해줍니다.
// verify
StepVerifier.create(getImages("test_id_new"))
.thenConsumeWhile(image -> {
System.out.println("thenConsumeWhile");
return image.getFileName().startsWith(namePrefix) &&
image.getUrl().startsWith(urlPrefix);
}).verifyComplete();
실패해야 함에도 불구하고 이 테스트는 성공해버립니다. 로깅도 찍혀있지 않구요.
empty publisher가 리턴되어 onNext
트리거가 실행되지 않았기에 thenConsumeWhile
은 스킵 되고, 곧바로 verifyComplete
로 직행하는 케이스입니다.
이를 방지하고 싶다면
테스트 결과가 위처럼 나와버리면 해당 테스트 케이스는 의미가 없어집니다. 이를 방지하려면 몇 가지 방법들이 있습니다.
// verify 2 - AtomicInteger로 검증
AtomicInteger aInteger = new AtomicInteger(0);
StepVerifier.create(getImages(id).log())
.thenConsumeWhile(image -> {
System.out.println("thenConsumeWhile");
aInteger.incrementAndGet();
return image.getFileName().startsWith(namePrefix) &&
image.getUrl().startsWith(urlPrefix);
}).verifyComplete();
assertThat(aInteger.get()).isGreaterThan(0);
// verify 3 - recordWith & thenConsumeWhile & consumeRecordedWith로 검증
StepVerifier.create(getImages(id).log())
.recordWith(ArrayList::new) // list에 record
.thenConsumeWhile(v -> true) // 반복
.consumeRecordedWith(images -> { // 반복 완료 된 list로 검증
assertThat(images.size()).isGreaterThan(0);
System.out.println("thenConsumeWhile");
images.forEach(v -> {
assertThat(v.getFileName().startsWith(namePrefix));
assertThat(v.getUrl().startsWith(urlPrefix));
});
}).verifyComplete();
// verify 4 - 단순히 list로 collect해서 검증
StepVerifier.create(getImages(id).collectList())
.consumeNextWith(images -> {
System.out.println("thenConsumeWhile");
assertThat(images.size()).isGreaterThan(0);
images.forEach(v -> {
assertThat(v.getFileName().startsWith(namePrefix));
assertThat(v.getUrl().startsWith(urlPrefix));
});
}).verifyComplete();
// verify1
StepVerifier.create(getImages("id").log())
.assertNext(image -> {
assertThat(image.getFileName().startsWith(namePrefix));
assertThat(image.getUrl().startsWith(urlPrefix));
})
.thenConsumeWhile(image -> {
System.out.println("thenConsumeWhile");
return image.getFileName().startsWith(namePrefix) &&
image.getUrl().startsWith(urlPrefix);
})
.verifyComplete();
- AtomicInteger 기반 검증
- recordWith & thenConsumeWhile & consumeRecordedWith 기반 검증
- Mono list로 collect해서 consumeNextWith로 검증
- assertNext & thenConsumeWhile
여러 방법이 있겠지만 아무래도 간단하다 보니.. 저는 마지막 방법을 주로 사용하는 것 같습니다.
혹시나 thenConsumeWhile
기반으로 테스트가 작성되어 있는 경우, 설명드린 케이스에 포함되진 않는지 점검하면 좋지 않을까 싶습니다.
이미지 출처
번외
Flux sequence 테스트시 말씀하신 assertNext/consumeNextWith를 여러번 사용하기도 애매하기에
thenConsumeWhile처럼 onNext signal을 반복 소비하면서 item emit을 보장하는 연산자가 하나 있었으면 좋겠어서 문의해봤는데 (가명: consumeNextWhile), 영어가 짧아서 전달된건지는 모르겠지만.. 결론은 현재 있는 연산자들로 위 예시에서 골라서 써야함..
https://github.com/reactor/reactor-core/issues/2832 (번역기 돌린거라 죄송합니다..)
'Programming > Reactive' 카테고리의 다른 글
6. Reactive Stream - Operator (0) | 2020.11.30 |
---|---|
5. Reactive Stream - Backpressure 와 Subscription (0) | 2020.11.09 |
4. Reactive Streams - Publisher, Subscriber, Subscription (0) | 2020.11.01 |
3. Reactive Streams - Duality (0) | 2020.11.01 |
2. Reactive Stream - Observer, Iterator, Reactive Stream (0) | 2020.11.01 |