본 글은 이전에 작성된 글 (링크) 에서 이어지는 글입니다.
개요: 테스트의 두 가지 방법
Feign 테스트하기: 직접 요청 보내기
직접 서버에 요청을 보내 테스트하면 다음의 이점을 누릴 수 있다.
- 테스트용 서버를 통해 통합 테스트처럼 진행할 수 있다.
- Mock Server를 통해 테스트할 경우, 실제 서버와 충돌이 일어나도 알 방법이 없다.
- 그러나 서버와 직접 통신하게 되면, 기능 외적인 문제에 대해 미리 대응할 수 있다.
코드는 아래와 같다. (Java로 작성)
//테스트용 서버에 맞게 환경을 조절하고, logger Level을 FULL로 지정하여 상세한 로그를 출력한다.
@SpringBootTest(properties = {"env=test", "feign.client.config.default.loggerLevel=FULL"})
@ActiveProfiles({"beta"})
public class MemberApiClientTest {
@Autowired
TestApiClient testApiClient;
@Test
public void getMemberInfo() {
Long id = 123456L; //테스트용 서버에 있는 값이어야 한다.
TestData res = testApiClient.get(id);
//이부분은 적당한 값으로 채워주면 된다. 값이 올바른지, 결과값의 id가 적절한지 등등....
assertTrue(res.isSuccess());
assertEquals(id, res.getResult().getMemberNo());
assertEquals(ServiceCode.LINE_GIFT, res.getResult().getServiceCode());
}
}
Mocking의 필요성
Feign 클라이언트의 다음과 같은 기능을 테스트한다고 가정해보자.
- POST/PUT 요청을 통해 서버에서 엔티티를 생성 혹은 수정하도록 요청하는 기능
- DELETE 요청을 통해 서버에서 엔티티를 삭제하도록 요청하는 기능
다음의 기능을 실제 서버와 연동하여 테스트할 경우, 아래와 같은 문제가 생긴다.
- 해당 서버 및 데이터베이스에 테스트시마다 작업을 요청하게 된다.
- 해당 서버나 데이터베이스가 작동하지 않고 있는 경우 테스트에 실패한다.
- 테스트 코드를 잘못 작성하여 삭제하면 안되는 엔티티를 삭제하거나, 불필요한 엔티티를 생성할 수 있다.
또한 클라이언트의 기능을 테스트하는 것이기 때문에, Feign 클라이언트를 mocking하는 것은 당연히 고려할 수 없다.
그렇다면, 요청을 보낼 외부의 서버를 mocking하는 것은 어떨까?
WireMock을 통한 서버 Mocking
서버를 Mocking하여 테스트를 수행하기 위한 여러 라이브러리가 존재한다.
- RestTemplate의 경우, MockRestServiceServer를 통해 테스트를 수행할 수 있다.
- Feign의 경우는 MockRestServiceServer 대신, WireMock을 활용할 수 있다.
WireMock은 웹 서비스를 스터빙하여 테스트 수행을 돕는 라이브러리로, 실제 웹 서비스에 연결할 수 있는 HTTP 서버를 구성한다.
- WireMock 서버를 구동시킬 경우 http://localhost:{portNumber}로 작동하는 HTTP 서버가 생성된다.
- 포트번호는 직접 지정해줄 수도 있고, dynamicPort 옵션을 통해 랜덤한 포트번호를 지정할 수도 있다.
// 직접 포트번호 지정
wireMockServer = WireMockServer(WireMockConfiguration.wireMockConfig().port(8080))
// 랜덤한 포트번호 지정
wireMockServer = WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort())
이제 실제 테스트 코드를 보자.
코드
코드는 간결함을 위해 Kotlin으로 작성했으나, Java 코드와는 큰 차이가 없다.
- 이 글을 쓰던 시점부터 다니던 회사의 kotlin migration이 진행되어서...글을 읽는 분들께는 양해를 부탁드린다.
예시 코드 (Kotlin)
// Test를 위한 Configuration Class
@Configuration
@EnableFeignClients
@EnableAutoConfiguration
class FeignTestConfiguration {
}
// Feign Class
@FeignClient(url = "\${api.url}")
interface MessageApiClient {
@GetMapping(value = ["/info"])
fun getInfo(request: Request): Response
}
// Test Class
@SpringBootTest(
properties = ["env=test", "feign.client.config.default.loggerLevel=FULL"],
classes = [FeignTestConfiguration::class]
)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
class MessageApiClientTest {
@Autowired
lateinit var apiClient: ApiClient
companion object {
@JvmStatic
val WIREMOCK_PORT = SocketUtils.findAvailableTcpPort()
@JvmStatic
lateinit var wireMockServer: WireMockServer
/**
* url을 변경해주는 메소드. @DynamicPropertySource를 통해 변경한다.
* 변경값이 application context에 영향을 주기 때문에, @DirtiesContext 설정이 클래스에 추가되어 있다.
*/
@JvmStatic
@DynamicPropertySource
fun addUrlProperties(registry: DynamicPropertyRegistry) {
registry.add("api.url") { "localhost:$WIREMOCK_PORT" }
}
/**
* @BeforeAll 로 작성된 코드는 static 메소드여야 한다.
* 그렇기 때문에, companion object 내에 포함하였다.
* addUrlProperties 메소드 내에서 wireMockServer 초기화시 해당 메소드의 역할이 불분명해 질 수 있다.
* 그렇기 때문에, @BeforeAll로 별도 지정하여 테스트 시작 전에 wireMockServer를 초기화 및 구동한다.
*/
@JvmStatic
@BeforeAll
fun setUp() {
wireMockServer = WireMockServer(WireMockConfiguration.wireMockConfig().port(WIREMOCK_PORT))
wireMockServer.start()
WireMock.configureFor("localhost", WIREMOCK_PORT)
}
/**
* @AfterAll 로 작성된 코드는 static 메소드여야 한다.
* afterTest 메소드는 테스트가 종료되면, wireMockServer를 종료시킨다.
*/
@JvmStatic
@AfterAll
fun afterTest() {
wireMockServer.stop()
}
}
@AfterEach
fun afterEach() {
// 각 테스트 종료시마다 wireMockServer를 reset하여 stub된 내용을 지운다.
wireMockServer.resetAll()
}
@Test
@DisplayName("요청을 보내면, 적절한 응답이 돌아온다.")
fun test() {
val request = Request(
userCode = "123abcd"
)
stubSuccessResponse()
val response = apiClient.getInfo(request)
Assertions.assertTrue(response.success)
Assertions.assertEquals(response.name, "KAFKA")
Assertions.assertEquals(response.isMarried, false)
}
// stub 로직을 별도 메소드로 분리했다. wireMockServer에 가상의 요청에 대한 가상의 답변을 준비한다.
private fun stubSuccessResponse() {
val responseJson = JsonBuilderFactory.buildObject()
.add("success", true)
.add("name", "KAFKA")
.add("isMarried", false))
.json.toString()
wireMockServer.stubFor(
WireMock.post(WireMock.urlPathEqualTo("/person"))
.withRequestBody(WireMock.containing("\"userCode\":\"123abcd\""))
.willReturn(
WireMock.aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json; charset=utf-8")
.withBody(responseJson)
)
)
}
}
로직이 매우 복잡해 보인다. 그 이유는, 해당 테스트가 아래의 과정을 거치기 때문이다.
- 우선 static으로 지정된 @DynamicPropertySource 메소드가 작동하여 feign이 사용할 url 정보를 변경한다.
- Feign.Builder를 통해 테스트하면 이 과정이 필요하지 않다. 다만 실제 사용하는 Feign Client와 최대한 근접한 환경에서 테스트하려면 이런 과정이 필요할 수 있다.
- 또한 이 과정은, 별도의 .properties나 .yml 파일에 url을 저장해 Feign에서 사용한다고 가정한다.
- 이후 테스트 코드가 시작할 때, @BeforeAll로 지정된 wireMockServer init 과정이 수행된다.
- 테스트를 진행한다. 각 테스트는 시작하면서 stub 로직을 실행한다.
- 개개의 테스트가 끝나면 @AfterEach로 지정된 메소드가 실행되면서 wireMockServer를 리셋한다. 이 과정에서 테스트 중 수행한 stub가 사라진다.
- 모든 테스트가 끝나면 @AfterAll로 지정된 메소드가 실행되면서 wireMockServer가 종료된다.
마치며
RestTemplate에 비해 Feign의 테스트에 대한 레퍼런스가 적어 구현하는데에 어려움이 많았다.
이 글을 참고하여, 이후 작성시 참고할 수 있다면 좋겠다.
잘못된 내용이 있다면 댓글 부탁드립니다. 늘 감사합니다.
반응형
'프로그래밍 > Spring Boot' 카테고리의 다른 글
Kotlin 에서 Slf4j를 통해 로깅하기 (1) | 2024.01.01 |
---|---|
Spring Cloud OpenFeign 사용하기 (0) | 2022.03.07 |
LocalDateTime 사용 시 주의할 몇몇 오류사례 (0) | 2021.10.27 |
[SpringBoot] 프로젝트에 Swagger 적용하기 (0) | 2021.09.06 |
[SpringBoot] application.properties를 yml로 교체하기 (0) | 2021.08.31 |
댓글