안녕하세요. 이번 포스팅에서는 외부 API에 의존적인 로직을 어떻게 테스트했는지에 대해서 이야기 해보고자 합니다. 다른 좋은 방법이 있다면 댓글로 알려주시면 감사하겠습니다! 저는 Mock Server를 활용하여 외부 서버를 Mocking 하는 방식으로 진행하였습니다.
모든 코드는 여기서 확인하실 수 있습니다! 😊
어플리케이션을 개발 하다보면 외부 API를 사용해야 할 일이 많습니다. 저의 경우는 카카오 로그인을 사용하여 어플의 인증(Authentication)을 구현하고 있었습니다. 저번 포스팅에서 얘기한 것처럼 카카오로부터 토큰을 받아오고 유저 정보를 받아 오기 위해, 외부 API 사용 로직이 포함되는데요!
카카오 로그인을 테스트할 때 우리는 자바 코드를 통해서 사용자의 아이디 비밀번호를 입력하고, Code를 받아올 수 없습니다. Code를 받을 수 없으니 당연히 토큰도 받을 수 없고 토큰을 받을 수 없으니 사용자 정보도 받아올 수 없겠죠?
그렇다면 백엔드 개발자는 프론트 없이 자신이 카카오 로그인 구현했어
라고 말할 수 없는것일까요?
외부 서버는 우리가 제어할 수 있는 대상이 아니기 때문에 Mocking하고 우리의 로직만을 테스트 한다면 완벽하게 동작해 라고는 말할 수 없지만 카카오 API가 문제 없으면 동작할거야 ****라고 말할 순 있을 것 같아요. 간단하게 외부 서버를 Mocking하는 것의 의미와 장점을 알아보고 제가 구현한 코드를 보여 드리겠습니다.
일반적으로 Controller 단위 테스트에서 Service를 Mocking하고 테스트를 진행하듯 외부 서버도 우리의 로직에서는 Mocking하고 우리의 로직만 테스트하고자 하는 맥락이에요. 결국 의존적인 오브젝트들을 대역을 사용함으로써 대역이 정상적이라는 가정하에 핵심 로직을 테스트하고자 한다는 관점이에요.
그렇다면 저는 어떻게 구현 했는지 간단하게 설명하겠습니다. 저는 카카오 로그인과 관련된 LoginService 오브젝트가 WebClient를 통해 카카오 서버로 요청을 보냅니다. LoginService의 테스트 코드는 아래와 같습니다!
@ExtendWith(MockitoExtension.class)
class KakaoAPIServiceTest {
private KakaoAPIService kakaoAPIService;
@Mock
private JwtTokenProvider jwtTokenProvider;
private ObjectMapper objectMapper;
private MockWebServer mockWebServer;
private String mockServerUrl;
private Dispatcher dispatcher;
private MockResponse tokenResponse;
private MockResponse userResponse;
@BeforeEach
void setUp() throws JsonProcessingException {
objectMapper = new ObjectMapper();
mockWebServer = new MockWebServer();
mockServerUrl = mockWebServer.url("/").toString();
kakaoAPIService = new KakaoAPIService(mockServerUrl, mockServerUrl,
SERVER_URI, CLIENT_ID_VALUE,
CLIENT_SECRET_VALUE, GRANT_TYPE_VALUE); // 1
tokenResponse = new MockResponse() // 2
.addHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.setResponseCode(HttpStatus.OK.value())
.setBody(objectMapper.writeValueAsString(createMockKakaoTokenResponse()));
userResponse = new MockResponse() // 3
.addHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.setResponseCode(HttpStatus.OK.value())
.setBody(objectMapper.writeValueAsString(createMockKakaoUserResponse()));
dispatcher = new Dispatcher() { // 4
@NotNull
@Override
public MockResponse dispatch(RecordedRequest request) {
if (request.getPath().contains(OAUTH_TOKEN_PATH)) {
return tokenResponse;
}
if (request.getPath().contains(USER_INFO_PATH)) {
return userResponse;
}
return new MockResponse().setResponseCode(404);
}
};
mockWebServer.setDispatcher(dispatcher);
}
@AfterEach
void shutdown() throws IOException {
mockWebServer.shutdown();
}
@DisplayName("Redirect 될 토큰 페이지 Url을 리턴한다.")
@Test
void createTokenUrlTest() {
when(jwtTokenProvider.createToken(KAKAO_ID)).thenReturn(ACCESS_TOKEN);
String url = new DefaultUriBuilderFactory().builder()
.path(SERVER_URI + LOGIN_CHECK_PATH)
.queryParam(ACCESS_TOKEN, ACCESS_TOKEN)
.build().toString();
assertThat(kakaoAPIService.createTokenUrl(
JwtTokenResponse.of(jwtTokenProvider.createToken(KAKAO_ID))))
.isEqualTo(url); // 5
}
@DisplayName("카카오 서버에 요청을 해 Mono 토큰 정보를 받아온다.")
@Test
void fetchOAuthTokenTest() {
StepVerifier.create(kakaoAPIService.fetchOAuthToken(CODE_VALUE))
.consumeNextWith(body -> assertThat(body).isEqualToComparingFieldByField(createMockKakaoTokenResponse()))
.verifyComplete(); // 6
}
@DisplayName("카카오 서버에 요청을 해 Mono UserInfo를 받아온다.")
@Test
void fetchOAuthUserInfoTest() {
StepVerifier.create(kakaoAPIService.fetchUserInfo(createMockKakaoTokenResponse()))
.consumeNextWith(body -> assertThat(body).isEqualToComparingFieldByField(createMockKakaoUserResponse()))
.verifyComplete(); // 7
}
}
외부 서버를 Mocking하는 것은 사실 의존하고 있는 오브젝트를 대역을 사용하는 것과 비슷한 것 같습니다. 의존하는 것에 대한 부분을 대역을 사용하고 자신의 고유 메소드를 테스트한다는 점에서요!
카카오 로그인을 완벽하게 구현했다라고 말할 순 없지만, 위와 같은 테스트를 추가함으로써 외부 API가 정상적으로 동작한다면 카카오 로그인은 정상 동작합니다. 라고 말할 수 있는 것만으로도 엄청난 가치 갖지 않을까 싶습니다! 감사합니다. (설정 정보 같은 걸 잘 못 적으면 여전히 안되겠지만요.. 반환하는 DTO나 요청 관련된 부분도 동일하게 테스트 해야 합니다!)
감사합니다!