Skip to content

03. How to use StepVerifier

이진혁 edited this page May 25, 2021 · 1 revision

FluxMono를 이용하여 asynchronous하고 non-blocking한 프로그래밍을 할 수 있었습니다.
하지만 이런 프로그래밍은 디버깅이 힘들고 테스트가 힘들다는 단점이 존재합니다.
이러한 문제를 웹플럭스의 StepVerifier가 테스트를 용이하게 도와줍니다.
오늘은 이런 StepVerifier에 대해 알아보겠습니다.


가장 간단한 Publisher 테스트

var flux = Flux.just("apple", "banana", "melon");
StepVerifier.create(flux)
    .expectNext("apple")
    .expectNext("banana")
    .expectNext("melon")
    .expectComplete()
    .verify();

'apple', 'banana', 'melon' 이라는 데이터가 들어있는 데이터 스트림을 테스트하고자 합니다.
그러면 데이터 스트림에서 순서대로 데이터를 뽑아냈을 때 apple -> banana -> melon 순서대로 데이터가 나와야 합니다.
이를 테스트하기 위해서 expectNext() 메소드를 사용하여 검증합니다.

StepVerifier.Step expectNext(T t)

Description:
Expect the next element received to be equal to the given value.

Parameters:
t - the value to expect

Returns:
this builder

이렇게 expectNext() 메소드를 통해 검증을 마치게 되면 결과적으로 아무 문제 없이 성공했는지 확인하기 위해
expectComplete() 메소드를 통해 다시 한 번 검증합니다.

StepVerifier expectComplete()

Description:
Expect the completion signal.

Returns:
the built verification scenario, ready to be verified

그런 다음 검증이 모두 마쳤다는 의미로 verify() 메소드로 검증을 끝마칩니다.

Duration verify() throws AssertionError

Description:
Verify the signals received by this subscriber.
Unless a default timeout has been set before construction of the StepVerifier via setDefaultTimeout(Duration),
this method will block until the stream has been terminated (either through Subscriber.onComplete(),
Subscriber.onError(Throwable) or Subscription.cancel()).
Depending on the declared expectations and actions,
notably in case of undersized manual requests, such a verification could also block indefinitely.

Returns:
the actual Duration the verification took.

Throws:
AssertionError - in case of expectation failures

이렇게 테스트 코드를 작성하게 되면 expectComplate()verify()가 계속 같이 사용되는 것을 볼 수 있습니다.
그래서 이 둘을 합친 숏컷인 verifyComplate()라는 메소드를 지원합니다.

var flux = Flux.just("apple", "banana", "melon");
StepVerifier.create(flux)
    .expectNext("apple")
    .expectNext("banana")
    .expectNext("melon")
    .verifyComplate();

Example

가장 간단한 Publisher 테스트

에러가 발생하는 것을 테스트

var flux = Flux.just("apple")
    .mergeWith(Flux.error(new NullPointerException()));

StepVerifier.create(flux)
    .expectNext("apple")
    .expectError(NullPointerException.class)
    .verify();

처음엔 'apple' 데이터를, 두 번째에는 NullPointerException을 발생시키는 데이터 스트림을 만들었습니다.
이를 테스트하기 위해서는 expectNext() 메소드를 통해 'apple' 데이터가 나오는지 확인하고,
expectError() 메소드를 통해 NullPointerException이 발생하는지 확인해야 합니다.

StepVerifier expectError(Class<? extends Throwable> clazz)

Description:
Expect an error of the specified type.
Parameters:
clazz - the expected error type
Returns:
the built verification scenario, ready to be verified

expectComplate()verify()를 합친 숏컷으로 verifyComplate()를 제공했던 것처럼,
expectError()verify()를 합친 숏컷으로 verifyError()를 제공합니다.
따라서 다음과 같이 사용할 수도 있습니다.

var flux = Flux.just("apple")
    .mergeWith(Flux.error(new NullPointerException()));

StepVerifier.create(flux)
    .expectNext("apple")
    .verifyError(NullPointerException.class);

여기서 헷갈렸던 점이 두 번째 데이터에서 에러가 발생하는 Flux를 만들기 위해서
Flux.just()를 사용했었던 것이 저를 헷갈리게 만들었습니다.
초기에는 데이터 스트림을 구성하는 코드가 다음과 같았습니다.

var flux = Flux.just("apple", new NullPointerException());

그래서 NullPointerException이 발생하는 데이터가 아니라
NullPointerException이 발생한 데이터가 되어 버려서 expectError()가 에러를 잡지 못했습니다.
Publisher가 에러를 발생시키는 데이터를 가지게 하기 위해서는
Flux.error() 또는 Mono.error()를 사용해야 한다는 것을 깨달았습니다.

Example

에러 발생 테스트

assertNext()를 이용한 테스트

Flux<Box> flux = Flux.just(new Box("apple"), new Box("banana"));

StepVerifier.create(flux)
    .assertNext(box -> Assertions.assertThat(box.getElement()).isEqualTo("apple"))
    .assertNext(box -> Assertions.assertThat(box.getElement()).isEqualTo("banana"))
    .verifyComplete();

처음엔 'apple' 데이터를 담은 Box 객체를, 두 번째에는 'banana' 데이터를 담은 Box 객체를 내보내는 데이터 스트림을 만들었습니다. 물론 Box 객체가 equals() 메소드를 구현하도록 하는 방법도 괜찮지만 기존에 있던 Assertions 라이브러리를 활용하는 것이 더 효율적입니다. 그래서 SpecVerifier API에서는 assertNext()라는 메소드를 지원합니다.

default StepVerifier.Step assertNext(Consumer<? super T> assertionConsumer)

Descriptions:
Expect an element and consume it with the given consumer, usually performing assertions on it
(eg. using Hamcrest, AssertJ or JUnit assertion methods).
Alias for consumeNextWith(Consumer) for better discoverability of that use case.
Any AssertionErrors thrown by the consumer will be rethrown during verification.

Parameters:
assertionConsumer - the consumer for the value, performing assertions

Returns:
this builder

Example

assertNext()를 이용한 테스트

interval() 테스트

interval() 메소드와 이에 따라오는 take() 메소드를 사용하는 작업은
실제로 시간을 기다리며 데이터를 생성하기 때문에
실질적인 시간이 오래걸릴 수 밖에 없습니다.
이를 해결하기 위해서 withVirtualTime() 메소드를 사용할 수 있습니다.
일단 이 메소드를 사용하기 전에 얼마나 테스트 코드가 오래 걸리는지 확인해봅시다.

Flux<Long> flux = Flux.interval(Duration.ofSeconds(10)).take(10);
StepVerifier.create(flux)
    .expectNextCount(10)
    .verifyComplete();

10초마다 하나의 데이터를 생성하는 무한한 데이터 스트림을 생성하고
여기에 10개의 데이터 제한을 두는 Flux 객체를 만들었습니다.

이를 테스트할 때는 StepVerifier 객체를 만들고 expectNextCount() 메소드를 이용해서 테스트하는데
본 메소드는 데이터의 본질을 보지 않고 데이터의 개수를 검증하는 메소드입니다.

StepVerifier.Step expectNextCount(long count)

Descriptions:
Expect to received count elements, starting from the previous expectation or onSubscribe.

Parameters:
count - the number of emitted items to expect.

Returns:
this builder

하지만 이렇게 테스트를 진행하면 StepVerifier가 실행되는 시간을 제외하더라도
총 100초의 시간이 걸리게 됩니다.
이러한 문제를 해결하기 위해서 우리는 StepVerifier에서 지원하는
withVirtualTime() 메소드를 사용할 수 있습니다.

StepVerifier.withVirtualTime(() -> Flux.interval(Duration.ofSeconds(10)).take(10))
    .thenAwait(Duration.ofSeconds(50))
    .expectNextCount(5)
    .thenAwait(Duration.ofSeconds(30))
    .expectNextCount(3)
    .thenAwait(Duration.ofSeconds(20))
    .expectNextCount(2)
    .verifyComplete();

실제로 위 테스트 코드를 withVirtualTime() 없이 실행하게 되면,
총 100초의 시간을 기다리게 됩니다.
하지만 thenAwait() 메소드를 이용해서 시간을 넘기고,
expectNextCount() 메소드를 이용해서 시간을 넘긴만큼
생긴 데이터를 검증하면 쉽게 interval()를 검증할 수 있습니다.

static StepVerifier.FirstStep withVirtualTime(Supplier<? extends Publisher<? extends T>> scenarioSupplier)

Descriptions:
Prepare a new StepVerifier in a controlled environment using VirtualTimeScheduler to manipulate a virtual clock via StepVerifier.Step.thenAwait().
The scheduler is injected into all Schedulers factories, which means that any operator created within the lambda without a specific scheduler will use virtual time. Each verify() will fully (re)play the scenario. The verification will request an unbounded amount of values.
Note that virtual time, StepVerifier.Step.thenAwait(Duration) sources that are subscribed on a different Scheduler (eg. a source that is initialized outside of the lambda with a dedicated Scheduler) and delays introduced within the data path (eg. an interval in a flatMap) are not always compatible, as this can perform the clock move BEFORE the interval schedules itself, resulting in the interval never playing out.

Type Parameters:
T - the type of the subscriber

Parameters:
scenarioSupplier - a mandatory supplier of the Publisher to subscribe to and verify.
In order for operators to use virtual time, they must be invoked from within the lambda.

Returns:
a builder for expectation declaration and ultimately verification.

StepVerifier.Step thenAwait(Duration timeshift)

Descriptions:
Pause the expectation evaluation for a given Duration.
If a VirtualTimeScheduler has been configured, VirtualTimeScheduler.advanceTimeBy(Duration)
will be used and the pause will not block testing or Publisher thread.

Parameters:
timeshift - a pause Duration
Returns:
this builder

이 이외에도 expectSubscription() 메소드와 expectNoEvent() 메소드를 이용해서 interval()을 검증할 수 있습니다.
expectNoEvent()는 매개변수로 들어온 Duration 시간 동안 아무 데이터도 들어오지 않으면 검증에 성공합니다. expectSubscription()expectNoEvent() 메소드를 사용하기 위해서 필요한 선 작업입니다.

StepVerifier.Step expectNoEvent(Duration duration)

Descriptions:
Expect that no event has been observed by the verifier for the length of the provided Duration.
If virtual time is used, this duration is verified using the virtual clock.
Note that you should only use this method as the first expectation if you actually don't expect a subscription to happen.
Use StepVerifier.FirstStep.expectSubscription() combined with expectNoEvent(Duration) to work around that.
Also avoid using this method at the end of the set of expectations:
prefer StepVerifier.LastStep.expectTimeout(Duration) rather than expectNoEvent(...).thenCancel().

Parameters:
duration - the duration for which to observe no event has been received

Returns:
this builder

StepVerifier.Step expectSubscription()

Descriptions:
Expect a Subscription. Effectively behave as the default implicit Subscription expectation.

Returns:
this builder

Example

interval() 메소드 테스트
withVirtualTime() 메소드를 이용한 interval() 메소드 테스트


Reference

백기선님 강의
StepVerifier Docs

Clone this wiki locally