โ Mock ์๋ฒ๋ฅผ ์ฌ์ฉํ๋ ์ด์
REST API ์์ฒญ์ ํ ์ ์๋๋ก ํ๋ RestTemplate · WebClient · FeignClient ํด๋์ค๋ฅผ ํ์ฉํ์ฌ ์ธ๋ถ API ์์ฒญ์ ํตํด ๊ธฐ๋ฅ์ ๊ตฌํํ์๋ค.
ํ ์คํธ ์ฝ๋๋ฅผ ์ง๊ธฐ ์ํ ๋ฐฉ๋ฒ์ ์ฐพ์๋ณด๋, Mock ์น ์๋ฒ๋ฅผ ์ ์ํด์ ํ ์คํธ ํ๋ ๋ฐฉ๋ฒ์ด ์์๋ค.
๐ก์ฅ์
1. ์ค์ API ์๋ฒ๋ฅผ ์ฌ์ฉํด์ ํ ์คํธ๋ฅผ ํ๋ฉด, ์๋ฒ ์ํ์ ๋ฐ๋ผ ํ ์คํธ์ ๊ฒฐ๊ณผ๊ฐ ๋ฌ๋ผ์ง ์ ์๋ค.
2. Mock ์๋ฒ๋ก๋, ์ธ๋ถ API๊ฐ ์ ์์ด๊ณ ์ ์์ ์ธ ๊ฐ(์์ํ ๊ฐ)์ด ๋ฐํ๋๋ค๋ฉด, ์ ์์ ์ผ๋ก ๋ก์ง์ด ์๋ํ๋ ๊ฒ์ ๋ณด์ฌ์ค ์ ์๋ค.
3. ๋ก์ปฌ์ ์๋ฒ๋ฅผ ๋์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ์๋๊ฐ ๋น ๋ฅด๋ค๋ ์ฅ์ ์ด ์๋ค.
์์ ๊ฐ์ ์ฅ์ ์ด ์๋ Mock ์น ์๋ฒ๋ฅผ ๊ตฌํํ๊ธฐ ์ํด์ ์ฌ์ฉํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋
๋ํ์ ์ผ๋ก MockWebServer
์ WireMock
์ด ์๋ค.
WireMock
์ด ์๋๋ก์ด๋ ํ๊ฒฝ์์ ๋ฌธ์ ๊ฐ ์๊ฒจ ํ์ํ ๊ฒ์ด MockWebServer
๋ผ๊ณ ํ๋ค.
์๋๋ก์ด๋ ํ๊ฒฝ์ด ์๋๋ผ๋ฉด WireMock
์ด ๊ธฐ๋ฅ์ด ํ๋ถํด์ ์ถ์ฒํ๋ค๋ stackoverflow ๊ธ์ด ์๋ค.
๋๋, 2๊ฐ์ง ๋ฐฉ๋ฒ ๋ชจ๋ ์ฌ์ฉํด์ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํด๋ณด๋๋ก ํ๊ฒ ๋ค.
๐ก ๋ ๋ฐฉ์์ ๊ณตํต์ ์ ๋ก์ปฌ์ ๊ฐ์ง ์๋ฒ๋ฅผ ๋์ด ๋ค, ํน์ ์์ฒญ์ ํ์ ๋์ ์๋ต๊ฐ์ ๊ฐ์ ํ๊ณ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ฒ์ด๋ค.
Mockito ํ ์คํธ ์ฝ๋๋ฅผ ์ง๋ ๋ฐฉ์๊ณผ ํก์ฌํ๋ค.
ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ ๋ก์ง
// github ์ธก์ clientId, clientSecret, code๋ฅผ ์ ๋ฌํด์ accessToken์ ๋ฐ๋ ๋ก์ง
@FeignClient(name = "access-token-feign-client", url = "${feign.client.access-token.url}")
public interface AccessTokenFeignClient {
@PostMapping("/login/oauth/access_token")
String getAccessToken(
@RequestParam("client_id") String clientId,
@RequestParam("client_secret") String clientSecret,
@RequestParam("code") String code
);
}
// acessToken์ AuthorizationHeader์ ๋ด์ ์์ฒญํด์ github ํ์ ์ ๋ณด๋ฅผ ๋ฐ๋ ๋ก์ง
@FeignClient(name = "user-profile-feign-client", url = "${feign.client.user-profile.url}")
public interface UserProfileFeignClient {
@GetMapping("/user")
UserProfile getUserInfo(@RequestHeader("Authorization") String accessToken);
}
FeignClient
๋ฅผ ์ด์ฉํด ๋ง๋ , ์ 2๊ฐ์ ๋ก์ง ํ
์คํธ ์ฝ๋๋ฅผ ์์ฑํด๋ณด๊ฒ ๋ค.
MockWebServer
Dependency ์ถ๊ฐ
testImplementation group: 'com.squareup.okhttp3', name: 'mockwebserver', version: '5.0.0-alpha.11'
implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '5.0.0-alpha.11'
build.gralde
์ ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ถ๊ฐํด์ค๋ค.
๊ทธ๋ฆฌ๊ณ ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋ฒ์ ์ ์ผ์นํด์ผ ํ๋ค๊ณ ํ๋ค.
Test Code ์์ฑ
@SpringBootTest
public class FeignClientTest {
@Autowired
private AccessTokenFeignClient accessTokenFeignClient;
@Autowired
private UserProfileFeignClient userProfileFeignClient;
public static MockWebServer mockWebServer = new MockWebServer();
@DynamicPropertySource
public static void addUrlProperties(DynamicPropertyRegistry registry) {
registry.add("feign.client.access-token.url", () -> "http://localhost:" + mockWebServer.getPort());
registry.add("feign.client.user-profile.url", () -> "http://localhost:" + mockWebServer.getPort());
}
@BeforeAll
public static void setUp() throws IOException {
mockWebServer.start();
}
@AfterAll
public static void shutdown() throws IOException {
mockWebServer.shutdown();
}
}
์ฐ๋ฆฌ๊ฐ ์์ฑํ MockWebServer
์ ์์ฒญ์ ๋ณด๋ด์ผ, ์ ์ํ ์๋ต์ ๋ฐ์ ์ ์์ ๊ฒ์ด๋ค.
@DynamicPropertySource
์ด๋
ธํ
์ด์
๊ณผ getPort()
๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ, ์ธ๋ถ api ์์ฒญ์ MockWebServer
๋ก ์์ฒญํ ์ ์๋๋ก ํ๊ฒฝ๋ณ์๋ฅผ ๋ณ๊ฒฝํ๋ค.
๊ทธ๋ฆฌ๊ณ , ๋งค ํ ์คํธ ์ฝ๋ ์คํ ์ ์๋ฒ๊ฐ ์คํ๋๊ณ ๋ฉ์๋ ์คํ ํ ๋ซํ๋๋ก ์ค์ ํด์ค๋ค.
@Test
@DisplayName("AccessToken ๊ฐ์ ธ์ค๊ธฐ ์ฑ๊ณต ํ
์คํธ")
public void getAccessTokenSuccess() throws InterruptedException {
//given
String clientId = "clientId";
String clientSecrets = "clientSecrets";
String code = "code";
String expectedToken = "token";
String expectedResponse = String.format("access_token=%s&expires_in={๊ฐ}&refresh_token={๊ฐ}&refresh_token_expires_in={๊ฐ}&scope=&token_type={๊ฐ}", expectedToken);
mockWebServer.enqueue(new MockResponse()
.setBody(expectedResponse));
//when
String response = accessTokenFeignClient.getAccessToken(clientId, clientSecrets, code);
//then
RecordedRequest recordedRequest = mockWebServer.takeRequest();
assertThat(recordedRequest.getMethod()).isEqualTo("POST");
assertThat(recordedRequest.getPath())
.isEqualTo(String.format("/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", clientId, clientSecrets, code));
assertThat(TextParsingUtil.parsingFormData(response).get("access_token")).isEqualTo(expectedToken);
}
enqueue()
๋ฉ์๋๋ฅผ ํตํด, mockWebServer
๋ก ์์ฒญ์ด ๋ค์ด์์ ๋, ์๋ต ๊ฐ์ ์ง์ ํด์ค๋ค.
๊ทธ๋ฆฌ๊ณ , FeignClient
๋ก ์ ์ํ ๋ฉ์๋ ์คํ ์ ์ ์๋ HTTP Method์ URI๋ก ์์ฒญ์ด ์ ๊ฐ๋์ง ํ์ธํด์ค๋ค.
static class GithubResponse {
private String login;
private String avatar_url;
private String blog;
private String html_url;
public GithubResponse(String login, String avatar_url, String blog, String html_url) {
this.login = login;
this.avatar_url = avatar_url;
this.blog = blog;
this.html_url = html_url;
}
}
@Test
@DisplayName("ํ์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ ์ฑ๊ณต ํ
์คํธ")
public void getUserProfileSuccess() throws InterruptedException {
//given
Gson gson = new Gson();
String userName = "userName";
String imageUrl = "imageUrl";
String blog = "blog";
String githubUrl = "githubUrl";
String expectedResponse = gson.toJson(new GithubResponse(userName, imageUrl, blog, githubUrl));
String accessToken = "accesstoken";
mockWebServer.enqueue(new MockResponse()
.addHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.setBody(expectedResponse));
//when
UserProfile response = userProfileFeignClient.getUserInfo(accessToken);
//then
RecordedRequest recordedRequest = mockWebServer.takeRequest();
assertThat(recordedRequest.getMethod()).isEqualTo("GET");
assertThat(recordedRequest.getPath())
.isEqualTo("/user");
assertThat(response.getUserName()).isEqualTo(userName);
assertThat(response.getImageUrl()).isEqualTo(imageUrl);
assertThat(response.getBlog()).isEqualTo(blog);
assertThat(response.getGithubUrl()).isEqualTo(githubUrl);
}
getUserInfo()
๋ฉ์๋์ ๊ฒฝ์ฐ๋ ์ฝ๊ฐ ๋ณต์กํ๋ค.
์ค์ ๋ก, ์ง๋ ฌํ๋ JSON ํํ๋ก ํ์ ์ ๋ณด๋ฅผ ๋ณด๋ด์ฃผ๋ฏ๋ก ์ด๋ฅผ ๋ชจ๋ฐฉํ๊ธฐ ์ํด์ GithubResponse
๋ผ๋ ์์ ํด๋์ค๋ฅผ ์ ์ ํ ๋ค, ์ง๋ ฌํ ์ํจ ๊ฐ์ ์๋ต๊ฐ์ผ๋ก ์ ์ํ๋ค.
์ง๋ ฌํ๋ ์๋ต ๊ฐ์, FeignClient
๋ ์ญ์ง๋ ฌํํ์ฌ UserProfile
์ด๋ผ๋ ๊ฐ์ฒด๋ฅผ ์์ฑํ ์ ์์ด์ผ ํ๋ค.
๋ฐ๋ผ์, ์์ ๊ฐ์ด HTTP Method ์ ์์ฒญ URI, ๊ทธ๋ฆฌ๊ณ ์ญ์ง๋ ฌํ๋ ๊ฐ์ฒด์ ๋ฐ์ดํฐ ๊ฐ์ด ๋์ผํ์ง ๊ฒ์ฆํจ์ผ๋ก์ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ ์ ์์๋ค.
WireMock
Dependency ์ถ๊ฐ
implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-contract-stub-runner', version: '4.0.1'
WireMock
์ ์ฌ์ฉํ๊ธฐ ์ํด, ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ถ๊ฐํด์ค๋ค.
Test Code ์์ฑ
@SpringBootTest
@AutoConfigureWireMock
public class FeignClientTest {
@Autowired
private AccessTokenFeignClient accessTokenFeignClient;
@Autowired
private UserProfileFeignClient userProfileFeignClient;
private static WireMockServer wireMockServer = new WireMockServer(8081);
@DynamicPropertySource
public static void addUrlProperties(DynamicPropertyRegistry registry) {
registry.add("feign.client.access-token.url", () -> "http://localhost:" + wireMockServer.port());
registry.add("feign.client.user-profile.url", () -> "http://localhost:" + wireMockServer.port());
}
@BeforeAll
public static void setUp() {
wireMockServer.start();
}
@AfterAll
public static void shutdown(){
wireMockServer.stop();
}
}
WireMock
๋ ์ด๊ธฐ ์ค์ ์ MockWebServer
์ ์ ์ฌํ๋ค.
๋จ, @SpringBootTest
์ ํจ๊ป ์ฌ์ฉํ๋ฉด 8080 ํฌํธ๋ฅผ Mock ์น ์๋ฒ์ ๊ฐ์ด ์ฌ์ฉํ ์ ์์ผ๋ฏ๋ก
WireMockServer
์ ํฌํธ๋ฅผ 8081๋ก ์ค์ ํด์ฃผ์๋ค.
๊ทธ๋ฆฌ๊ณ @AutoConfigureWireMock
์ด๋
ธํ
์ด์
์ ๋ถ์ฌ์ค๋ค.
@Test
@DisplayName("AccessToken ๊ฐ์ ธ์ค๊ธฐ ์ฑ๊ณต ํ
์คํธ")
public void getAccessTokenSuccess() {
//given
String clientId = "clientId";
String clientSecrets = "clientSecrets";
String code = "code";
String expectedToken = "token";
String expectedResponse = String.format("access_token=%s&expires_in={๊ฐ}&refresh_token={๊ฐ}&refresh_token_expires_in={๊ฐ}&scope=&token_type={๊ฐ}", expectedToken);
wireMockServer.stubFor(post(urlEqualTo(String.format("/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", clientId, clientSecrets, code)))
.willReturn(
aResponse()
.withBody(expectedResponse)
));
//when
String response = accessTokenFeignClient.getAccessToken(clientId, clientSecrets, code);
//then
assertThat(TextParsingUtil.parsingFormData(response).get("access_token")).isEqualTo(expectedToken);
}
static class GithubResponse {
private String login;
private String avatar_url;
private String blog;
private String html_url;
public GithubResponse(String login, String avatar_url, String blog, String html_url) {
this.login = login;
this.avatar_url = avatar_url;
this.blog = blog;
this.html_url = html_url;
}
}
@Test
@DisplayName("ํ์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ ์ฑ๊ณต ํ
์คํธ")
public void getUserProfileSuccess(){
//given
Gson gson = new Gson();
String userName = "userName";
String imageUrl = "imageUrl";
String blog = "blog";
String githubUrl = "githubUrl";
String expectedResponse = gson.toJson(new GithubResponse(userName, imageUrl, blog, githubUrl));
String accessToken = "accesstoken";
wireMockServer.stubFor(get(urlEqualTo("/user"))
.willReturn(
aResponse()
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withBody(expectedResponse)
));
//when
UserProfile response = userProfileFeignClient.getUserInfo(accessToken);
//then
assertThat(response.getUserName()).isEqualTo(userName);
assertThat(response.getImageUrl()).isEqualTo(imageUrl);
assertThat(response.getBlog()).isEqualTo(blog);
assertThat(response.getGithubUrl()).isEqualTo(githubUrl);
}
ํ
์คํธ ์ฝ๋๋ก ๊ฒ์ฆํ๋ ๋ฐฉ์์ MockWebServer
์ ์ ์ฌํ ํ๋ฆ์ผ๋ก ์์ฑํ์๋ค.
๋ค๋ฅธ ์ ์
wireMockServer.stubFor(post(urlEqualTo(String.format("/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", clientId, clientSecrets, code)))
.willReturn(
aResponse()
.withBody(expectedResponse)
));
์ ๋ถ๋ถ ์ฒ๋ผ, Mocking ํด์ค ๋ Http Method๋ ์ง์ ํ ์ ์๋ค.
์ค์ ๋ก ์ ์ฝ๋์์, Http Method๋ฅผ get์ผ๋ก ๋ณ๊ฒฝํ๋ ๊ฒฝ์ฐ, ํ ์คํธ ์คํจ๋ฅผ ํ๊ฒ ๋๋ค.
์๋ํ๋ฉด, getAccessToken()
๋ฉ์๋์์ POST ์์ฒญ์ ๋ณด๋ด๋๋ก ๋์ด์๊ธฐ ๋๋ฌธ์ด๋ค.
MockWebServer
์ ๋น๊ตํ์ ๋, ๋ณด๋ค ๊ตฌ์ฒด์ ์ธ ์ํฉ์ผ๋ก ํ
์คํธ ์ฝ๋๋ฅผ ์์ฑํ ์ ์์ด์
์ฝ๋๊ฐ ์ด๋ค ์ญํ ์ธ์ง ๋ ์ ๋ณด์ฌ์ค ์ ์๋ค๋ ์ฅ์ ์ด ์๋ ๊ฒ ๊ฐ๋ค.