개발을 할때 기능을 최대한 작게 만들고, 독립적으로(어느 기술에 의존하지 않고) 만들려고 노력한다. 이번 외부 API와 연동할때에도 'API와의 통신없이 실제와 같은 테스트를 할 수 있을까?'에 대해 고민하게 되었다.
일반적으로 테스트에는 의존성을 최소화하기 위해 Mockito를 사용하여 테스트를 진행하지만, 이는 완전한 해결법은 아닌 것 같았다. 외부 API를 사용할때는 http 요청과 응답, 응답값에 대한 역직렬화 과정 등이 발생하는데 Mockito는 단순히 목 객체를 주입받아서 메서드를 호출하는 방식으로 동작하기 때문이다. 목 객체를 주입받으면 실제 http 통신을 통해 받은 응답값을 확인할 수 없기 때문에 뭐랄까.. 테스트를 위한 테스트? 같은 느낌이었다. 나는 실제 통신까지 한 값을 받고 싶다고!
만약 실제 외부 API를 호출하는 방식은 어떨까? 외부 API 서버의 상태에 따라 테스트 결과가 달라지게 되니 좋은 테스트라고 할 수 없다.
열심히 서치를 하던 중 WireMock이라는 라이브러리를 통해 테스트하는 방식을 발견했다.
1. WireMock?
Http 기반의 API서비스를 Mocking하는 용도로 제공되는 Mock서버 라이브러리.
지정해둔 uri로 요청이 발생할 경우 목 서버로 http 요청이 발생하고 미리 지정해둔 형태의 http 응답이 반환된다.
이를 통해, 실제 외부 API에 의존하지 않으면서 http 요청/응답을 통한 테스트가 가능해진다. 또한 로컬 서버를 사용하기 때문에 테스트 속도도 빠르다.
2. 프로젝트에 적용
<build.gradle>
spring cloud에서 제공하는 의존성을 추가했다.
dependencies {
testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner:4.1.2'
}
<WireMockConfig>
@TestConfiguration
public class WireMockConfig {
@Bean(initMethod = "start", destroyMethod = "stop")
public WireMockServer wireMockService() {
return new WireMockServer(0);
}
}
API를 호출할 모의 서버를 빈으로 등록한다.
포트 번호는 랜덤으로 사용하기 위해 0으로 설정했다.
<application-test.yml>
openai:
url: http://localhost:${wiremock.server.port}
테스트 url을 넣어준다.
로컬 서버를 사용하면서, 포트 번호는 랜덤이기 때문에 ${wiremock.server.port}를 사용했다.
<ai-response.json>
{
"id": "chatcmpl-abc123",
"object": "chat.completion",
"created": 1677858242,
"model": "gpt-3.5-turbo-0613",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "This is a test!"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 13,
"completion_tokens": 7,
"total_tokens": 20
}
}
미리 생성한 응답을 만들어둔다. OpenAI에서 제공하는 AI 응답 예시를 넣어두었다.
파일 경로: src/test/resources/__files/payload/ai-response.json
파일을 생성한 Path 때문에 애를 먹었는데, 여러 블로그 글을 보니 __files 디렉토리에 대한 글을 못 찾았었다. 그래서 저게 뭐지... 자동으로 생성해주는 건가 싶어서 한참 고민하고 있었는데 그냥 직접 해당 디렉토리를 생성해주니 해결되었다.
공식 문서를 살펴보면 payload 디렉토리 하위 json 파일을 응답값으로 사용한다고 한다.
<feign-mapping.json>
{
"request": {
"method": "POST",
"url": "/v1/chat/completions"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"bodyFileName": "payload/ai-response.json"
}
}
WireMock으로 사용하는 로컬 서버에 stub을 설정하는 파일을 만들어둔다. 여기서는 목 서버를 사용하는 곳에서 명시적으로 stub을 세팅하기 위해 응답값만 json 파일로 만들어두었다.
- stub: http 요청 path와 그에 대한 http 응답을 설정해서 제공하는 것.
쉽게 말해, 가짜 API를 만드는 것이다.
파일 경로: src/test/resources/mappings/feign-mapping.json
공식 문서에서 WireMock 서버는 mappings 디렉토리 하위 json 파일을 읽어서 stub을 설정한다고 한다.
여기까지 일단 정리해보면,
- 테스트에서 feign-mapping에 정의된 api로 WireMock에 요청하면
- WireMock은 그에 맞는 응답을 반환한다. 위에서는 ai-response.json이 반환되게 했다.
<AiMocks>
public class AiMocks {
public static void setupAiMockResponse(WireMockServer mockService) {
mockService.stubFor(post(urlPathEqualTo("/v1/chat/completions"))
.withHeader("Authorization", equalTo("test-key"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBodyFile("payload/ai-response.json")
)
);
}
}
WireMock에서 제공하는 stubfor(), post()를 사용해 이 클래스에서 실제 stub을 설정해주었다.
임의로 만든 모의 서버를 호출하면 어떤 응답이 와야하는지 작성한다.
세팅은 끝났다.
이제 테스트코드만 열심히 작성하면 된다.
테스트 코드 및 기타 설정 확인은 => https://github.com/SpinLog/backend/blob/main/src/test/java/com/example/spinlog/ai/service/OpenAiClientTest.java
backend/src/test/java/com/example/spinlog/ai/service/OpenAiClientTest.java at main · SpinLog/backend
Contribute to SpinLog/backend development by creating an account on GitHub.
github.com
외부 API를 사용하는 코드를 어떻게 독립적으로 테스트를 할 수 있을까 고민하던 중, WireMock은 로컬 서버를 실제 api와 유사하게 통신할 수 있도록 도와줘서 잘 선택한 것 같다. 최대한 작게, 다른 서비스의 의존성 없이 코드를 만들 수 있어서 뿌듯하다.
하지만 설정이 꽤나 복잡하다는 점, 자료가 그리 많지 않다는 점(이건 잘 못 찾아본 것일 수 있음), 무엇보다 이 설정 때문에 한 3일 정도를 날렸다는 점에서 애증의 코드가 된 것 같다.
그럼 외부 API 연동은 여기서 끝!
참고 자료:
'개발 활동 > 스위프' 카테고리의 다른 글
[스위프 후기] 백엔드 개발자의 스위프 참여 후기(Spinlog) (1) | 2024.06.11 |
---|---|
외부 API 연동 - 외부 API에 문제가 있으면? Circuit Breaker Pattern & Resilience4j (0) | 2024.05.30 |
외부 API 연동하기 - OpenFeign을 이용한 ChatGPT 연동 (0) | 2024.05.30 |
외부 API 연동하기 - 종류 (0) | 2024.05.30 |
[Test] 애플리케이션 테스트 방법 (0) | 2024.04.29 |