Hexagonal Architecture의 테스트코드 작성 (feat. StepVerifier, TestPublisher)
프로젝트를 Hexagonal Architecture로 개발하면서, 외부 시스템과 소통하는 Port 인터페이스를 통해서 실제 비즈니스를 담당하는 UseCase의 구현체인 Service를 검증할 수 있다.
독립적인 테스트 코드
- 비즈니스 로직이 외부 시스템에 의존하지 않으므로, 독립적으로 단위 테스트(Unit Test), 빠른 테스트 실행
- DataBase, HTTP Client, 메시징 등의 외부 시스템에 대한 각 Adapter는 독립적으로 테스트 가능
의존성의 대체(Mock, Stub, Fake)
- Port-Adapter 패턴으로 인해, 실제 사용하는 외부 시스템이 아닌 Mock 등 으로 활용한 테스트 가능
- 예를 들어, DB Adapter 대신 InMemoryRepository를 사용하여 테스트할 수 있다.
테스트코드 설계 단계에서 러프하게 짜둔 목표는 다음과 같다.
1. 애플리케이션 서비스(UseCase)에 대한 테스트는 Mock 기반으로 대체해서 단위 테스트
2. 실제 외부 시스템과 연결되는 Adapter 같은 경우는 통합 테스트로 진행
@Test
public void 이미_좋아요를_누른_사용자의_updateLikes(){
// given
when(findLikePort.isUserLiked(boardId, userId)).thenReturn(Mono.just(true));
when(findLikePort.getLikesCount(boardId)).thenReturn(Mono.just(2L));
when(removeLikePort.removeLike(boardId, userId)).thenReturn(Mono.empty());
// when
when(findLikePort.getLikesCount(boardId)).thenReturn(Mono.just(1L));
Mono<Long> result = updateLikeService.updateLikes(userId, boardId);
// then
StepVerifier.create(result)
.expectNext(1L)
.verifyComplete();
verify(removeLikePort).removeLike(boardId, userId);
verify(findLikePort).getLikesCount(boardId);
}
비즈니스 영역 안에서는 어떤 외부 시스템을 사용하는지 알 수 없으며, Port 인터페이스를 통한 느슨한 의존은 테스트를 좀 더 가독성 있고 쉽게 작성하게 해준다.
클린 아키텍처를 내부적으로 구현했다고 볼 수 있는 헥사고날 아키텍처가 왜 유지보수성이 뛰어난지, 왜 테스트 용이성이 뛰어나다고 하는건지 직접 적용해보며 체감할 수 있었다.
Project Reactor의 테스트 도구, StepVerifier
Reactor에서 비동기 Publisher 시퀀스(Mono나 Flux) 동작을 검증하는데 사용되는 도구
- verify() 메서드를 호출하면 테스트가 실행되며, 예상과 다른 경우 AssertionError를 발생시킨다.
- expectNext() , expectNextMatches() , assertNext() 등의 메서드를 통해 Publisher가 특정 데이터를 방출하는지 확인할 수 있다.
- thenRequest(long)이나 thenCancel()을 통해서 특정 시점에 요청을 보내거나 구독을 취소할 수 있다.
StepVerifier (reactor-test 3.7.3)
Prepare a new StepVerifier in a controlled environment using a user-provided VirtualTimeScheduler to manipulate a virtual clock via StepVerifier.Step.thenAwait(). The scheduler is injected into all Schedulers factories, which means that any operator create
projectreactor.io
Dependency
reactor-test 의존성을 추가해서 적용할 수 있다.
testImplementation 'io.projectreactor:reactor-test:3.6.5'
StepVerifier의 기능
StepVerifier.create(result)
- Mono<T> 또는 Flux<T>의 결과를 검증하는 시작점
expectNext(10L)
- Mono에서 방출된 예상값을 비교
verifyComplete()
- 스트림이 정상적으로 완료되었는지 확인
- 에러 발생 시 테스트 실패
verifyComplete() : 완료 이벤트가 발생해야 테스트 통과한다. 여기서 완료 이벤트는 onComplete()를 말한다.
expectComplete().verify() : Complete 이벤트를 예상하며, 전체 흐름을 검증한다.
Mono<Long> result = updateLikeService.updateLikes(userId, boardId);
StepVerifier.create(result)
.expectNext(10L)
.verifyComplete();
위 코드는 updateLikeService.updateLikes(userId, boardId)의 결과가 Mono<Long>이고, 그 값이 10L인지 확인한다
StepVerifier가 필요한 이유?
Spring WebFlux 환경에서는 비동기로 동작하기 때문에 JUnit만으로는 검증이 어렵다.
→ 비동기 흐름을 유지하면서 검증
block()을 사용하면 테스트 가능하지만, 블로킹 방식이라 WebFlux의 비동기 철학과 맞지 않음
다양한 Publisher를 생성하는 TestPublisher
Subscriber의 동작을 검증하기 위해서 직접 Publisher를 구현해야하며, 백프레셔를 테스트하거나 특정 이벤트에 대한 동작을 검증할 수 있다.
reactor-test 라이브러리에서 제공하는 TestPublisher를 통해 다양한 이벤트를 발생시키는 Publisher를 생성할 수 있다.
백프레셔(Backpressure)
Reactive Stream을 통해 스트림 형태의 데이터를 처리할때 발생하는 upstream과 downstream의 속도 차이를 조절한다.
요청이 있었는지 백프레셔를 검증하는 메서드 : assertWasRequestd(), assertMinRequested(long n) , assertMaxRequested(long n)
외에도 emit(), next(), complete() 등의 데이터 방출과 구독 확인에 대한 기능을 제공해서 추상화된 스트림 데이터 처리를 검증할 수 있다.
@Test
void testPublisher(){
TestPublisher<Integer> publisher = TestPublisher.create();
publisher.subscribe(new Subscriber<Integer>() {
@Override
public void onSubscribe(Subscription s) {
s.request(10);
}
@Override public void onNext(Integer o) { }
@Override public void onError(Throwable t) { }
@Override public void onComplete() { }
});
publisher.assertWasRequested();
...
}
간단한 기능을 알아보자
publisher.assertWasRequested();
publisher.assertMinRequested(10);
publisher.assertMaxRequested(10);
subscribe() : Publisher에게 구독자(subscriber) 등록을 요청한다
assertWasRequested() : upstream으로부터 데이터 요청이 있었는지 확인한다(min, max)
publisher.emit(1, 2);
publisher.assertWasNotCancelled();
emit(1,2) : 데이터를 방출한다.
assertWasNotCancelled() : 구독이 취소되지 않았는지 확인한다.
publisher.assertSubscribers(1);
publisher.assertNoSubscribers();
assertSubscribers(1) : 구독자가 1명 있는지 확인한다
assertNoSubscribers() : 구독자가 없는지 확인한다. (구독자가 남아있기 때문에 오류가 난다.)