Contents
- 시작하며
- TestContainers : 컨테이너를 활용한 통합테스트 품질 향상하기
- WireMock : 외부 의존 서버를 쉽게 Stubbing 하기
- Reactive Streams 테스트의 기본기 : StepVerifier
- WebFlux의 Controller 단위테스트 방법
- WebFlux의 Stubbing
- WebFlux의 Service 단위테스트
- WebFlux의 MongoRepository 단위테스트
- WebFlux의 Integration Test
- JUnit의 Parameterzied Test
- JUnit의 Repeated Test
- 추천 온라인 강의
- 마치며Contents
시작하며
지난 블로그에서는 테스트 코드를 작성하고, 커버리지 95% 이상 유지하며 깨달은 것들을 공유했습니다. 테스트 철학, 전략, 노하우, Jacoco 등의 고수준 개념(High-Level concept)을 다뤘습니다. 이번에는 F-Lab 멘토링 과정에서 진행한 프로젝트에서 테스트 코드를 작성하고, 커버리지 95% 이상 유지하며 익힌 것들을 공유하려고 합니다.
아직 Reactor, Spring WebFlux 테스트 관련 코드는 시중에 많이 없기 때문에 공유하면 좋겠다는 생각이 들었습니다 제가 제시하는 테스트 작성 방법들은 유일한 방법이 아니며 더 효율적인 WebFlux 테스트 코드가 시중에 많이 생겨나기를 바랍니다.
TestContainers : 컨테이너를 활용한 통합테스트 품질 향상하기
JUnit + Spring 생태계에서 대부분의 데이터베이스는 테스트목적 embedded 버전이 라이브러리로 존재합니다. 예시로 MongoDB의 경우 de.flapdoodle.embeddedMongo 가 있습니다. embedded 버전의 의존 서비스를 사용하는 것도 현명하지만
embedded 버전은 동일하게 동작해도 동일하게 구현되지는 않았다는 점을 고려해야합니다.
이 점은 데이터베이스에 국한되어 있지 않은 개념으로 테스트 프로세스에서 실제 의존서비스를 대체하는 컴포넌트들 모두에 해당되는 부분입니다. 만약 어느 개발자가 @PostConstruct로 중요한 로직을 작성하는 바람에 서비스의 성공/실패 여부가 MongoDB와의 초기연결 + warm up 프로세스에 의존한다면, 가볍게 embeddedMongoDB를 사용할 수 있을까요?
이런 부분을 최소화하고자 나온 라이브러리가 TestContainers입니다. TestContainers는 Docker Container로 실행가능한 모든 것을 활용할 수 있게 도와주는 라이브러리입니다. TestContainer 활용 예시는 이전 블로그 에서도 확인이 가능합니다. 이번 섹션의 결론은 컨테이너 활용을 통해 더욱 설득력있는 테스트를 작성하자 였습니다.
2022.01.13 - [우아한 중계서버] - [Reactive한 라이더위치 기능구현 ] 요구사항 분석부터 위치정보 저장 기능 구현까지
WireMock : 외부 의존 서버를 쉽게 Stubbing 하기
WireMock을 은 이전 블로그에서 상세하게 다룬적이 있기 때문에 블로그 링크를 공유하며 High Level View만 설명합니다.
2022.01.13 - [우아한 중계서버] - [Reactive한 라이더위치 기능구현 ] 요구사항 분석부터 위치정보 저장 기능 구현까지
WireMock은 버전 X 부터 Spring Cloud 에 포함된 테스트 라이브러리다. Spring Cloud 에 포함된만큼 WireMock은 분산환경의 서비스에서 HTTP를 Stubbing 하는 기능을 제공합니다.
예를들어 테스트하는 메소드가 인접한 서버의 URL "http://12.12.32.5:3131/relay/v1/shop/{id}" 로 요청을 전송하는 부분이 있다면 굳이 해당 서버를 띄울 필요없이 WireMock으로 response를 Stubbing 할 수 있습니다. 또한 WireMock 의 직관적인 Fluent 한 네이밍은 사용도를 더욱 높입니다.
여기서는 WireMock의 기능을 프로젝트의 WireMock 활용 케이스 하나를 공유하겠습니다.
1번
스프링 어플리케이션 컨텍스트에 WireMock을 주입해주는 부분. HTTP 포트/ HTTPS 포트 / stub file 위치 등을 어노테이션에서 설정할 수 있습니다. port = 0 은 다이나믹 포트 바인딩
2번
wireMock의 stub 을 작성하는 부분입니다.
- Line 41
- post() : HTTP 메소드를 지정하고 모든 메소드 동일
- urlPathEqualTo : URL 매칭하는 방법들 중하나, urlPathMatching(String regEx) 처럼 REGEX 로도 가능하다.
- urlPathEqualTo("/relay/v1/shop/" + delivery.getShopId()) urlPathMatching(proxyAgencyUrl + "/.*")
- Line 42 : willReturn() : 응답 stub 을 설정하기 시작하는 부분. willReturn 이전에 withHeader, withParam 등 여러가지 설정이 존재합니다.
- Line 46 : 여기서는 Reactor 의 retry 기능 테스트 용도로 사용했다.
- Line 54 : Mockito의 verify.... times... 처럼 매칭되는 요청이 몇번 들어왔는지도 확인 가능하다.
- ***주의사항 : count의 스코프가 메소드인지 클래스인지 알고 사용하기
3번
WireMock 은 Stub 뿐만 아니라 Mockito.verify()와 같은 검증도 가능합니다.
위의 예시처럼 스프링 어플리케이션 컨텍스트 로드 과정에서 WireMock의 설정정보를 바인딩할 수 있습니다.
마지막으로 WireMock 관련 팁은 Stubbing 의 스코프를 염두해두자 입니다. Test 라이프 싸이클이 메소드/클래스에 따라 결과가 달라질 수 있기 때문입니다.
Reactive Streams 테스트의 기본기 : StepVerifier
핵심은 Reactive Stream 입니다. 제가 아는 한 Publisher(Mono, Flux)의 스트림을 검증하는 대부분의 테스트는 StepVerifier 를 활용합니다. StepVerifier는 대상 스트림을 구독하고 리액티브의 행위, 순서, 시간 등을 테스트 할 수 있는 API 입니다. Flux와 Mono 학습 도중 테스트 코드가 없다면 Baeldung의 StepVerifier 튜토리얼 추천합니다.
추가로 리액티브 스트림의 Hot & Cold 개념도 익히는 것을 추천합니다.
StepVerifier 관련한 내용은 SpringMVC 와 SpringWebFlux의 테스트 작성 예시를 비교하며 마치겠습니다.
기존 블로킹 방식에서는 메소드 실행 후 리턴된 변수를 가지고 검증합니다.
@Test
void hi() {
// given
var someObj = new generateMethod();
// when
var actual = someInstance.doSomething();
// then
Assertions.assertEquals(expected, actual);
}
Non-Blocking 방식에서는 소스코드의 리액티브한 부분과 이벤트를 테스트합니다.
@Test
void hi() {
// given
var someObj = new SomeObj();
// when
var someReactiveStream = someMethod(someObj);
// then
StepVerifier.create(someReactiveStream)
.expectNext(expected)
.verifyComplete();
}
WebFlux의 Controller 단위테스트 방법
설정하기
- Line 33 : @WebFluxTest 는 WebFlux의 @Controller 의 단위테스트를 위한 셋업을 해줍니다.
- 여기서 셋업의 의미는 Controller의 RequestMapping 된 엔드포인트로 요청전송이 가능하다는 것.
- Line 38 : 반대로 이야기하면 Service 클래스는 null 이기때문에 모킹해야 합니다.
- Line 34 : @AutoConfigureWebTestClient : WebTestClient는 내부적으로 WebClient를 활용하고 Fluent API 를 통해 응답을 검증합니다.
- Line 37 : WebTest 클라이언트는 @Autowired 해서 사용합니다. 아래 사용법 예시의 uri 부분을 보면 알겠지만 host:port를 자동으로 바인딩 해주기때문에 서버의 baseUrl을 명시할 필요가 없습니다.
WebTestClient 사용법
후반부에 다시 언급하겠지만 IntegrationTest 에서도 WebTestClient를 사용하기때문에 가능하면 어느정도 재사용성을 높힐 방법을 찾는것도 좋습니다.
방법 1. 일부만 FluentAPI 로 검증하고 나머지는 변수로 받아서 별도의 검증(Assertion) 을 진행하는 방법
필자가 (고안해내서 그런지) 가독성이 높은(다고 생각하는) 방법입니다. 완전(fully) Fluent 하게 작성하지는 않지만 실행(요청) 단계와 검증 단계를 어느 정도 분리 할 수 있습니다.
가독성이 높은 Fluent API 작성이 불가능하다면 Assertions을 활용하자 는게 제 의견입니다.
방법 2. WebTestClient 가 추구하는 Fluent한 방법
WebTestClient.BodySpec<>에 consumeWith() 메소드를 체이닝해서 사용합니다. 저도 처음 이 방법을 사용했지만 테스트가 너무 세로로 길어지는 경우가 많아 요즘은 방법 1을 사용합니다.
WebFlux의 Stubbing
MVC 모델과 다르지 않습니다. 리턴타입만 Reactive 할 뿐.
WebFlux의 Service 단위테스트
Service 단위테스트는 리턴 타입으로 Publisher를 주고받는다는 점 외에는 Sprign MVC 와 동일합니다. Mocking 과 Stubbing으로 주변 의존 컴포넌트들의 behavior 를 조작하며 테스트합니다. 아래는 가볍게 던지는 예시입니다.
WebFlux의 MongoRepository 단위테스트
@DataMongoTest 는 MongoDB 관련 테스트에 필요한 셋업을 해준다. 여기에는 Mongo@WebFluxTest와 매우 유사하다. MongoDB 관련 설정만 set 해주고 내장 in-memory MongoDB 프로세스에 default로 바인딩 됩니다. (존재할 경우)
이후에는 다른 Reactor 테스트 방법과 동일합니다.
WebFlux의 Integration Test
Integration Test 는 위의 단위테스트들을 통합한 것이라고 생각하면 단순하며 실제로도 그렇습니다. 개인적으로 SpringMVC에서보다 단순하게 느껴집니다. 단순하다는 의미를 예시 단 하나로 모든 것을 표현하고 코드 관련 설명도 진행하겠습니다.
위에서 소개한 Controller 단위테스트와 다른점은 클래스 레벨 설정 어노테이션 @SpringBootTest 하나입니다. Controller에 필요한 부분만 생성하는 @WebFluxTest 와 달리 통합테스트에서는 @SpringBootTest 어플리케이션 컨텍스트 전체를 Start Up 합니다.
JUnit의 Parameterzied Test
JUnit의 @ParameterizedTest 기능을 통해 하나의 테스트 메소드에서 다른 변수들로 여러번 테스트할 수 있습니다. @ParameterizedTest 사용이 항상 효과적인 것은 아니니 따져보며 적용해야합니다.
@ParameterizedTest가 유용한 상황은 이렇습니다.
- 공통된 behavior(행위)를 가지고 서로다른 value(값)을 핸들링하는 경우
- 메소드 단위의 테스트 작성하는 경우
- 여러 경우의 수를 테스트하는 경우
- 각 테스트간의 차이점을 변수로 분리가능한 경우
경우의 수를 다루거나 때로는 테스트 대상 메소드의 성공/실패 케이스도 묶을 수있습니다. 하지만 꼭 묶어야한다는 욕심은 버려야합니다. OOP의 재사용성이 저주가 될수 있기 때문에.
@Parameterized 테스트 예시를 하나 살펴보겠습니다. ExceptionHandlerAdivce 가 Exception 별 설정해놓은 HttpStatus 코드를 제대로 리턴하는지를 확인하는 테스트입니다. 첫번째 파라미터로 Exception 을, 두번째로 설정한 HttpStatus 코드를 리턴합니다.
- Line 31 : 메소드 이름은 바로 위 Line 292와 일치해야합니다.
- Line 32 : Stream.of(Arguments...)를 리턴해야합니다. 참고로 여기서 이야기하는 Stream 은 자바8의 그 StreamAPI 가 맞다.
- Line 43 : @ParameterizeTest 는 대상 메소드가 ParameterizedTest라는 것을 명시해준다.
- Line 45 : ParameterizedTest는 Arguments를 Stream 으로 반환하는 ArgumentSource를 필수적으로 명시해줘야합니다. @MethodSource는 그 중 하나일뿐. Fully-Qualified-Name으로 입력해줘야하며 메소드의 경우 Stream<Arguments>를 리턴하는 static 메소드여야합니다.
- 테스트 실행 결과
@ParameterizedTest 사용시 주의할점
- 파라미터가 너무 방대해지는지 확인하기
- 파라미터가 너무 많아지고 가독성이 떨어지다보면 별도의 메소드로 분리하는 것을 고려해야합니다.
- 테스트 메소드 내부의 불필요한 반복
- ParameterizedTest의 실행 횟수에 비례해서 메소드 실행시간도 늘어난다는 것을 염두해둬야합니다.
- 유틸리티 클래스 초기화 처럼 한번만 실행하면 되는 부분들은 테스트 메소드 외부로 분리해줍니다.
- 아래 간단한 예시를 공유합니다. @ArgumentSource로 1000번실행되는 메소드가 설정되어있습니다. 테스트 메소드 내부에서 Random 클래스 초기화를 1000번 하게된다는 의미입니다. 이 부분을 바깥으로 분리해줍니다.
JUnit의 Repeated Test
지정한 테스트를 명시된 횟수만큼 반복하는 테스트입니다. 이해한 바로는 실행결과가 항상 같은지를 테스트하거나, 항상 같지 않은지를 테스트를 할 수 있습니다. 프로젝트에서 사용한 적이 없어서 샘플코드는 없고 힌트만 남깁니다.
추천 온라인 강의
내돈 내산 스프링부트/WebFlux 리액티브 마이크로서비스 온라인 강의입니다. (광고아닙니다.) 영어로 학습이 힘드신 분들에게는 아쉽지만 영어로 되어있습니다. 쉴세없이 강의료를 만원~이만원대로 훅 깎아버리는 Udemy의 온라인강의입니다.
이 강의의 장점은 프로젝트의 많은 부분들을 테스트 코드까지 작성하는 법을 배울 수 있다는 점과, 중간중간 Netty의 Event Loop Thread Model 과 같은 Core Concept 도 알기 쉽게 설명해준다는 점입니다.
제 생각에 이 강의 하나 들으신다면 아마 당장 프로젝트 시작하기에는 어려움이 없을 것이라 봅니다.
https://www.udemy.com/course/build-reactive-restful-apis-using-spring-boot-webflux/
마치며
'in-bob-we-trust' 카테고리의 다른 글
대용량 트래픽은 어떻게 테스트해야할까? 60만 RPM 테스트하기 (K6, AWS, EC2, 테스트, 성능테스트) (10) | 2022.06.09 |
---|---|
메시지큐를 활용한 비동기 통신에 대해서 알아보자 (0) | 2022.04.01 |
테스트를 작성하고, 커버리지를 95% 이상 유지하며 깨달은 것들 (테스트 철학, 전략, 노하우, Jacoco) (0) | 2022.01.31 |
서버 성능테스트 이야기 4 [ 성능개선 적용기 : StackTrace 최소화 ] (0) | 2022.01.28 |
서버 성능테스트 이야기 3 [ 성능개선 : JVM 메모리 영역 확장 ] (0) | 2022.01.28 |