이전 게시글에서 'WebClient를 사용해서, 외부 API를 호출하기'라는 글을 작성했습니다.
https://velog.io/@da_na/WebClient-WebClient-사용해서-외부-API-호출하기
저희 서비스는 크롤링 서버를 따로 분리했기 때문에 WebClient를 사용해서 Spring Boot(AWS EC2)가 외부 API인 크롤링 서버를 호출하는 방식으로 되어 있습니다.
따라서 아래의 WebClient 서비스 로직은 Spring Boot가 외부 API 크롤링 서버를 호출하는 로직입니다.
@Service
public class WebClientService {
@Transactional
public WebClientBodyResponse crawlingItem(String crawlingApiEndPoint, String pageUrl) {
Map<String, Object> bodyMap = new HashMap<>();
bodyMap.put("url", pageUrl);
WebClient webClient = WebClient.builder().baseUrl(crawlingApiEndPoint).build();
try {
WebClientResponse webClientResponse = webClient.post()
.bodyValue(bodyMap)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse -> {
throw new RuntimeException("4xx");
})
.onStatus(HttpStatus::is4xxClientError, clientResponse -> {
throw new RuntimeException("5xx");
})
.bodyToMono(WebClientResponse.class)
.block();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return objectMapper.readValue(
webClientResponse != null ? webClientResponse.getBody() : null,
WebClientBodyResponse.class);
} catch (Exception e) {
return null;
}
}
}
이전의 테스트 코드를 도입하기 전까지는 POSTMAN을 사용하여 직접 외부 서버와 통신하고 값을 확인했습니다.
따라서, 테스트가 외부 서버에 매우 의존적이었고 서버를 직접 호출하기 때문에 테스트 시간이 오래 걸렸습니다.
그리고 이전 게시글의 마지막에 WebClient를 리팩토링하는데, WebClient로 부터 받은 값을 변환하는 과정을 변경할 때에도 계속 외부 서버와 통신하는 것도 계속 동일한 값을 받는 데에 외부 서버를 호출하는 것은 비효율적이라고 생각했습니다.
더 나아가, 400번 대, 500번 대의 외부 서버 에러를 발생시키려면 일부러 에러를 요청하는 크롤링 웹 페이지 URL을 찾아서 보내줘야 해서 에러 처리하기가 어려웠습니다.
그러면 직접 외부 서버와 통신하는 방법 말고는 다른 방법으로 테스트하는 방법은 없을지 고민하다가, MockWebServer를 선택하여 테스트에 도입할 수 있었습니다.
Square 팀에서 만든 MockWebServer는 HTTTP Request를 받아서 Response를 반환하는 간단하고 작은 웹서버입니다.
WebClient를 사용하여, Http를 호출하는 메서드의 테스트 코드를 작성할때 이 MockWebServer를 호출하게 함으로써 쉽게 테스트 코드를 작성할 수 있습니다.
그리고 실제로 Spring Team도 MockWebServer를 사용하여 테스트하라고 권장한다고 합니다.
MockWebServer 공식 문서 : https://github.com/square/okhttp/tree/master/mockwebserver
참고 자료 : https://www.devkuma.com/docs/mock-web-server/
Spring Team 출처 : https://github.com/spring-projects/spring-framework/issues/19852#issuecomment-453452354
testImplementation 'com.squareup.okhttp3:mockwebserver'
@Autowired
private WebClientService webClientService;
private MockWebServer mockWebServer;
private String mockWebServerUrl;
@BeforeEach
void setUp() throws IOException {
mockWebServer = new MockWebServer();
mockWebServer.start();
mockWebServerUrl = mockWebServer.url("/v1/crawling").toString();
}
@AfterEach
void terminate() throws IOException {
mockWebServer.shutdown();
}
@Test
void should_title_is_returned_When_the_webClient_server_responds_successfully() {
//given
mockWebServer.enqueue(new MockResponse()
.setResponseCode(200)
.setBody(mockScrap)
.addHeader("Content-Type", "application/json"));
//when
WebClientBodyResponse webClientBodyResponse = webClientService.crawlingItem(
mockWebServerUrl, "https://www.naver.com");
//then
assertThat(webClientBodyResponse.getTitle()).isEqualTo("서울역");
}
@Test
void should_it_returns_null_When_webClient_server_responds_unsuccessfully() {
//given
mockWebServer.enqueue(new MockResponse()
.setResponseCode(400)
);
//when
WebClientBodyResponse webClientBodyResponse = webClientService.crawlingItem(
mockWebServerUrl, "https://www.naver.com");
//then
assertThat(webClientBodyResponse).isEqualTo(null);
}
전체적인 MockWebServer를 이용한 테스트 코드입니다!
@SpringBootTest
@ActiveProfiles("test")
public class WebClientServiceTest {
@Autowired
private WebClientService webClientService;
private MockWebServer mockWebServer;
private String mockWebServerUrl;
private final String mockScrap = "{\n"
+ " \"statusCode\": 200,\n"
+ " \"headers\": {\n"
+ " \"Content-Type\": \"application/json\"\n"
+ " },\n"
+ " \"body\": \"{\\\"type\\\": \\\"place\\\", \\\"page_url\\\": \\\"https://map.kakao.com/1234\\\", "
+ "\\\"site_name\\\": \\\"KakaoMap\\\", \\\"lat\\\": 37.50359439708544, \\\"lng\\\": 127.04484896895218, "
+ "\\\"title\\\": \\\"서울역\\\", \\\"address\\\": \\\"서울특별시 중구 소공동 세종대로18길 2\\\", "
+ "\\\"phonenum\\\": \\\"1522-3232\\\", \\\"zipcode\\\": \\\"06151\\\", "
+ "\\\"homepageUrl\\\": \\\"https://www.seoul.co.kr\\\", \\\"category\\\": \\\"지하철\\\"}\"\n"
+ "}\n";
@BeforeEach
void setUp() throws IOException {
mockWebServer = new MockWebServer();
mockWebServer.start();
mockWebServerUrl = mockWebServer.url("/v1/crawling").toString();
}
@AfterEach
void terminate() throws IOException {
mockWebServer.shutdown();
}
@Test
void should_title_is_returned_When_the_webClient_server_responds_successfully() {
//given
mockWebServer.enqueue(new MockResponse()
.setResponseCode(200)
.setBody(mockScrap)
.addHeader("Content-Type", "application/json"));
//when
WebClientBodyResponse webClientBodyResponse = webClientService.crawlingItem(
mockWebServerUrl, "https://www.naver.com");
//then
assertThat(webClientBodyResponse.getTitle()).isEqualTo("서울역");
}
@Test
void should_it_returns_null_When_webClient_server_responds_unsuccessfully() {
//given
mockWebServer.enqueue(new MockResponse()
.setResponseCode(400)
);
//when
WebClientBodyResponse webClientBodyResponse = webClientService.crawlingItem(
mockWebServerUrl, "https://www.naver.com");
//then
assertThat(webClientBodyResponse).isEqualTo(null);
}
}
@Service
public class WebClientService {
@Value("${crawling.server.post.api.endPoint}")
private String crawlingApiEndPoint;
@Transactional
public JSONObject crawlingItem(String pageUrl) throws ParseException {
Map<String, Object> bodyMap = new HashMap<>();
bodyMap.put("url", pageUrl);
WebClient webClient = WebClient.builder().baseUrl(crawlingApiEndPoint).build();
Map<String, Object> response = webClient.post()
.bodyValue(bodyMap)
.retrieve()
.bodyToMono(Map.class)
.block();
JSONParser jsonParser = new JSONParser();
Object obj = jsonParser.parse(response.get("body").toString());
JSONObject jsonObject = (JSONObject) obj;
return jsonObject;
}
}
@BeforeEach
void setUp() throws IOException {
mockWebServer = new MockWebServer();
mockWebServer.start();
mockWebServerUrl = mockWebServer.url("/v1/crawling").toString();
System.out.println("mockWebServerUrl = " + mockWebServerUrl);
}
WebClientBodyResponse webClientBodyResponse = webClientService.crawlingItem(mockWebServerUrl, "https://www.naver.com");
@Service
public class WebClientService {
@Transactional
public WebClientBodyResponse crawlingItem(String crawlingApiEndPoint, String pageUrl) {
Map<String, Object> bodyMap = new HashMap<>();
bodyMap.put("url", pageUrl);
WebClient webClient = WebClient.builder().baseUrl(crawlingApiEndPoint).build();
try {
WebClientResponse webClientResponse = webClient.post()
.bodyValue(bodyMap)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse -> {
throw new RuntimeException("4xx");
})
.onStatus(HttpStatus::is4xxClientError, clientResponse -> {
throw new RuntimeException("5xx");
})
.bodyToMono(WebClientResponse.class)
.block();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return objectMapper.readValue(
webClientResponse != null ? webClientResponse.getBody() : null,
WebClientBodyResponse.class);
} catch (Exception e) {
return null;
}
}
}
@Test
void should_it_returns_null_When_webClient_server_responds_unsuccessfully() {
// webClient 서버가 정상적으로 응답을 주지 않는 경우, null로 반환되는지 확인
//given
mockWebServer.enqueue(new MockResponse()
.setResponseCode(400)
);
//when
WebClientBodyResponse webClientBodyResponse = webClientService.crawlingItem(
mockWebServerUrl, "https://www.naver.com");
//then
assertThat(webClientBodyResponse).isEqualTo(null);
}
@Test
void should_other_type_of_scrap_is_saved_When_webClientService_crawlingItem_returns_null() throws ParseException {
// webClientService.crawlingItem()이 null을 반환할 때, Other 타입의 Scrap이 저장되는지 확인
//given
memoRepository.deleteAll();
scrapRepository.deleteAll();
BDDMockito.when(webClientService.crawlingItem("http://localhost:123", pageUrl))
.thenReturn(null);
User user = userRepository.findById(1L).get();
//when
//then
assertThat(scrapService.saveScraps(user, pageUrl)).isInstanceOf(Other.class);
assertThat(scrapRepository.findByPageUrlAndUserAndDeletedDateIsNull(pageUrl, user)
.isPresent()).isTrue();
}