이전 챕터까지 해서 로컬 환경에서 여러 Spring Cloud 기술을 사용해서 실행 환경은 구축했습니다.
그러나 MSA 구조로 전환하면서 마이크로 서비스 간의 통신을 OpenFeign을 통해 하기 때문에 테스트들을 리팩토링해야 했습니다.
이번 챕터에서는 OpenFeign을 테스트하는 방법을 간략하게 알아보도록 하겠습니다.
1. OpenFeign 테스트 2가지 방법
OpenFeign를 테스트하는 방법으로는 크게 다음과 같은 2가지 방법이 존재할 것 같습니다.
- 실제 OpenFeign 통신을 거친 테스트
- OpenFeign 통신만 Mocking(Stubing)하여 테스트
2가지 방법에는 장단점이 각각 존재합니다.
1-1. 실제 OpenFeign 통신을 거치는 테스트
실제로 프로덕션 환경과 동일하게 테스트 환경에서도 통신하는 마이크로 서비스에 OpenFeign 요청을 보내서 통신하는 방법입니다.
장점
- 실제 프로덕션 환경과 동일하게 테스트할 수 있다. (신뢰성 Good)
단점
- 마이크로 서비스가 여러 개일 때 하나의 마이크로 서비스를 테스트하려고 하면 관련된 마이크로 서비스가 모두 실행중이어야 한다.
- 테스트 시간이 상대적으로 길다.
- 오류를 디버깅하기가 어렵다.
1-2. Mocking 테스트
Mocking 테스트는 OpenFeign으로 다른 마이크로 서비스와 통신하는 부분만 Stubing하는 방법입니다.
장점
- 상대적으로 테스트 시 빠른 피드백을 받을 수 있다. (테스트 시간이 빠르다)
- 마이크로 서비스 인프라 환경에 구애받지 않고 테스트가 가능하다.
단점
- 실제 수행과 테스트 결과가 다를 수 있다. (Stubing의 문제점)
이렇게 2가지 테스트가 존재할 때, Mocking 테스트를 했을 때 실제 결과와 다를 수 있다는 점이 있었지만
실제 OpenFeign을 통한 테스트는 다른 마이크로 서비스 환경에 종속적이라는 점이 더 불편했기 때문에
실제 마이크로 서비스 간 통신을 사용하는 것보다, Mocking을 사용하는 테스트로 진행했습니다.
2. OpenFeign Mocking 테스트 : Spring Cloud Contract WireMock
Spring Cloud 환경에서 테스트를 할 때,
테스트 지원 라이브러리인 Spring Cloud Contract를 사용하여 더 나은 테스트 환경을 구축할 수 있습니다.
자세한 Spring Cloud Contract 설명은 아래의 공식 문서를 참고하면 좋을 것 같습니다.
2-1. Spring Cloud Contract WireMock 의존성 추가
Gradle에서 Spring Cloud Contract WireMock의 의존성을 추가해줬습니다.
testImplementation 'org.springframework.cloud:spring-cloud-contract-wiremock'
2-2. OpenFeign Client Url 옵션 설정 파일로 관리 리팩토링
테스트를 위해서 기존 코드에서 OpenFeign 코드를 리팩토링 해야합니다.
테스트 시에 WireMock 서버를 실행해서 해당 WireMock 서버에 OpenFeign 요청을 보내야합니다.
이때, 테스트 환경에서 WireMock 서버의 endpoint로 요청을 보내도록 하기 위해 설정 파일로 변수를 관리하겠습니다.
// Before
@FeignClient(name = "member-service", path = "/api/members", fallback = MemberFeignFallback.class)
public interface MemberFeignClient {
...
}
// After
@FeignClient(name = "member-service", url = "${member.find.feign-endpoint}", path = "/api/members", fallback = MemberFeignFallback.class)
public interface MemberFeignClient {
...
}
@FeignClient의 url 옵션을 설정 파일에서 읽어오도록 위와 같이 수정했습니다.
기존에는 name 옵션으로 Eureka 서버의 서비스 이름을 지정해서
Default로 `https://${name}` (https://member-service)가 지정되어 동작했었습니다.
테스트 환경에서는 WireMock 서버로 요청을 보내게 하기 위해서 url 옵션을 설정 파일 변수로 설정해서 관리했습니다.
2-3. WireMock Server 테스트 환경 구성
@AutoConfigureWireMock(port = 0)
@TestPropertySource(properties = {
"member.find.feign-endpoint=http://localhost:${wiremock.server.port}"
})
public abstract class OpenFeignClientTest {
@Autowired
private WireMockServer wireMockServer;
@BeforeEach
void setUp() {
wireMockServer.stop();
wireMockServer.start();
}
@AfterEach
void afterEach() {
wireMockServer.resetAll();
}
}
위와 같이 따로 추상 클래스로 OpenFeign의 테스트 책임을 가진 클래스로 분리하여 관리했습니다.
- @AutoConfigureWireMock : 해당 어노테이션을 사용해서 WireMockServer를 주입받아서 사용할 수 있습니다.
- port = 0 : Wire Mock Server의 포트를 지정합니다. 기본 포트는 8080이고, 0을 지정하면 랜덤 포트로 실행됩니다.
- 저는 포트 충돌 방지 및 보안을 위해 0으로 랜덤 포트로 사용했습니다.
- port = 0 : Wire Mock Server의 포트를 지정합니다. 기본 포트는 8080이고, 0을 지정하면 랜덤 포트로 실행됩니다.
- @TestPropertySource : 해당 어노테이션으로 테스트 변수를 파일이 아닌 해당 클래스에서 관리했습니다.
- properties 옵션으로 앞서 OpenFeignClient의 endpoint를 실행하고 있는 WireMock 서버로 설정했습니다.
- ${wiremock.server.port}를 사용하면 실행되고 있는 랜덤 포트의 WireMock 서버의 포트를 가져올 수 있습니다.
- @BeforeEach : 각 테스트 메소드 실행 전에 WireMock 서버를 중지 후 실행하는 용도로 사용했습니다.
- @AfterEach : 각 테스트 메소드 후에 WireMock 서버의 기록을 Reset하도록 설정했습니다.
- 각 테스트 메소드 &클래스 별로 독립된 WireMock 서버 환경을 구축하기 위해 설정했습니다.
2-4. WireMock을 사용한 테스트 코드 리팩토링
@Test
@DisplayName("작성에 성공한다.")
void success() throws JsonProcessingException {
// given
final String loginId = "sh111";
final String expectedResponse = objectMapper.writeValueAsString(
Map.of(
"memberId", 1,
"nickname", "성하"
)
);
// WireMock 사용
stubFor(get(urlEqualTo("/api/members?loginId=" + loginId))
.willReturn(aResponse()
.withStatus(HttpStatus.OK.value())
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withBody(expectedResponse))
);
final BoardWriteRequest request = new BoardWriteRequest("newTitle", "newContent");
// when
final Long savedBoardId = boardService.writeBoard(loginId, request);
// then
assertThat(savedBoardId).isNotNull();
}
테스트 코드를 위와 같이 리팩토링 했습니다!
테스트 수행 시 발생하는 작업은 다음과 같습니다.
- Board Service에서 게시글 작성 로직 수행
- Member 정보를 얻기 위해 Member 마이크로 서비스로 OpenFeign 요청
- 이 과정을 WireMock Server를 사용해서 Stubing
Stubing을 수행한 부분을 자세히 살펴보면 다음과 같습니다.
final String expectedResponse = objectMapper.writeValueAsString(
Map.of(
"memberId", 1,
"nickname", "성하"
)
);
// WireMock 사용
stubFor(get(urlEqualTo("/api/members?loginId=" + loginId))
.willReturn(aResponse()
.withStatus(HttpStatus.OK.value())
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withBody(expectedResponse))
);
- expectedResponse : Stub할 Response를 구성하기 위해 ObjectMapper의 writeValueAsString과 Map을 사용했습니다.
- WireMock.stubFor : 해당 메소드를 사용하여 Stub을 시작합니다.
- WireMock.get & WireMock.urlEqualTo : Stub할 OpenFeign의 Http Method & URI를 지정해줍니다.
- MappingBuilder.willReturn : 응답 관련 설정을 시작합니다.
- WireMock.aResponse : ResponseDefinitionBuilder를 생성하여 Stub할 응답 스펙을 정의합니다.
- ResponseDefinitionBuilder.withStatus : Response Status를 설정합니다.
- ResponseDefinitionBuilder.withHeader : Response Header를 설정합니다.
- ResponseDefinitionBuilder.withBody : Response Body를 설정합니다. (생성한 expectedResponse 지정)
이렇게 리팩토링 후 테스트를 실행하면, 다음과 같이 Stub이 정상적으로 동작하여 테스트가 성공하는 것을 알 수 있습니다.
이렇게 WireMock을 사용하면, 마이크로 서비스의 실행 여부와 상관없이 독립적으로 테스트가 가능해집니다.
여기까지 WireMock을 사용하여 OpenFeign 테스트를 하는 방법을 알아봤습니다!
🎯 Github Repository 링크 (전체 코드)
https://github.com/sh111-coder/sh-board-msa
📘 Monolithic to MSA 전체 목차
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (1) MSA란?
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (2) 멀티 모듈 구성하기
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (3) Service Discovery 패턴 적용하기(feat. Spring Cloud Eureka)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (4) API Gateway 구현(feat. Spring Cloud Gateway)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (5) 서비스 간 통신하기(feat.Spring Cloud OpenFeign)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (6) 각 서비스의 설정 파일 관리하기(feat. Spring Cloud Config)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (7) 서비스 장애 대응 Circuit Breaker 구현(feat. Resilience4J)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (10) MSA 전환 후 비교 및 회고 + 마무리
Reference
https://docs.spring.io/spring-cloud-contract/reference/project-features-wiremock.html