티스토리 뷰
외부 API를 테스트할 때 실제 API를 호출해서 테스트 코드를 작성할 수도 있겠지만
그렇게 되면 다양한 응답값에 대한 테스트의 어려움이 있을 수 있고 외부 API 상태에 따라 테스트 코드가 실패하는 경우가 발생할 수 있다.
그런 경우 MockWebServer를 이용하여 WebClient를 Mocking해서 다양한 테스트 케이스를 작성할 수 있다.
MockWebServer
A scriptable web server for testing HTTP clients
Motivation
This library makes it easy to test that your app Does The Right Thing when it makes HTTP and HTTPS calls. It lets you specify which responses to return and then verify that requests were made as expected.
Because it exercises your full HTTP stack, you can be confident that you're testing everything. You can even copy & paste HTTP responses from your real web server to create representative test cases. Or test that your code survives in awkward-to-reproduce situations like 500 errors or slow-loading responses.
Http Client 테스트를 위한 스크립트 가능한 웹 서버로, 애플리케이션이 Http 또는 Https 호출을 할때 제대로 동작하는지 쉽게 테스트 할 수 있다. 이를 통해 반환할 응답을 지정한 다음 제대로 요청이 이뤄졌는지 확인할 수 있고, 응답지연이나 Http Status Error 500 같은 케이스도 테스트 할 수 있다.
사용법
사용법은 굉장히 간단하다. 아래와 같이 라이브러리를 추가 한다.(적용 환경 springboot v2.5.2 /gradle-7.0.2)
testImplementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.10'
testImplementation 'com.squareup.okhttp3:mockwebserver:5.0.0-alpha.10'
아래와 같이 테스트 전 MockWebServer를 생성하고, 테스트 종료 후 종료시키면 된다.
public class MockWebServerTest {
public MockWebServer mockWebServer;
@BeforeEach
static void setUp() {
mockWebServer = new MockWebServer();
}
@AfterEach
static void tearDown() throws IOException {
mockWebServer.shutdown();
}
}
Http 호출시 응답하는 Response 데이터는 아래와 같이 enqueue 메서드를 통해 추가할 수 있다.
그러 Http 호출이 이루어질 때 MockWebServer에서 enqueue에 넣은 데이터를 반환할 것이다.
@Test
void test() throws Exception {
//given
TestResponse response = new TestResponse("055055", "tester");
mockWebServer.enqueue(new MockResponse()
.setBody(objectMapper.writeValueAsString(response))
.addHeader("Content-Type", "application/json"));
//when
...
Http Call
//then
Verify
}
실제 적용 예제
간단하게 github api를 호출하여 repository에 대한 정보들을 받는 예제이다.
@Slf4j
@Component
public class WebClientConfig {
@Bean
public WebClient testWebClient() {
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1500)
.doOnConnected(conn ->
conn.addHandlerLast(new ReadTimeoutHandler(1500, TimeUnit.MILLISECONDS))
.addHandlerLast(new WriteTimeoutHandler(1500, TimeUnit.MILLISECONDS)));
return WebClient.builder()
.filters(functions -> {
functions.add(logRequest());
functions.add(logResponse());
})
.clientConnector(new ReactorClientHttpConnector(httpClient))
.baseUrl("https://api.github.com")
.build();
}
}
@Configuration
@RequiredArgsConstructor
public class TestHandler {
private final WebClient testWebClient;
public Flux<GitHubDto> clientDemo6(String userName) {
if (!StringUtils.hasText(userName)) {
throw new RuntimeException("user is Required");
}
Flux<GitHubDto> values = testWebClient.get()
.uri("/users/{name}/repos", userName)
.retrieve()
.bodyToFlux(GitHubDto.class);
return values;
}
}
@Data
public class GitHubDto {
private String id;
@JsonProperty("full_name")
private String fullName;
@JsonProperty("open_issues_count")
private int issueCount;
}
class TestHandlerTest {
MockWebServer mockWebServer;
TestHandler handler;
ObjectMapper objectMapper = new ObjectMapper();
@BeforeEach
void setUp() {
mockWebServer = new MockWebServer();
WebClient webClient = WebClient.create(mockWebServer.url("/github").toString());
handler = new TestHandler(webClient);
}
@AfterEach
void shutDown() throws IOException {
mockWebServer.shutdown();
}
@DisplayName("webMockServer Test")
@Test
public void test() {
//given
mockWebServer.enqueue(
new MockResponse().setResponseCode(400)
);
//when
StepVerifier.create(handler.clientDemo6("055055"))
.expectError();
}
@DisplayName("webMockServer Test- 200")
@Test
public void test_200() throws JsonProcessingException {
//given
GitHubDto dto1 = new GitHubDto();
dto1.setId("1622941913451");
dto1.setFullName("055055/bank");
dto1.setIssueCount(0);
GitHubDto dto2 = new GitHubDto();
dto2.setId("1600580264324");
dto2.setFullName("055055/market");
dto2.setIssueCount(0);
GitHubDto dto3 = new GitHubDto();
dto3.setId("444423408312312");
dto3.setFullName("055055/user");
dto3.setIssueCount(0);
List<GitHubDto> gitHubDtoList = Arrays.asList(dto1, dto2, dto3);
String response = objectMapper.writeValueAsString(gitHubDtoList);
mockWebServer.enqueue(
new MockResponse().setBody(response)
.addHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
);
//when
StepVerifier.create(handler.clientDemo6("055055"))
.assertNext(result -> {
assertThat(result.getId()).isEqualTo(dto1.getId());
assertThat(result.getFullName()).isEqualTo(dto1.getFullName());
assertThat(result.getIssueCount()).isEqualTo(dto1.getIssueCount());
}
)
.assertNext(result -> {
assertThat(result.getId()).isEqualTo(dto2.getId());
assertThat(result.getFullName()).isEqualTo(dto2.getFullName());
assertThat(result.getIssueCount()).isEqualTo(dto2.getIssueCount());
}
)
.assertNext(result -> {
assertThat(result.getId()).isEqualTo(dto3.getId());
assertThat(result.getFullName()).isEqualTo(dto3.getFullName());
assertThat(result.getIssueCount()).isEqualTo(dto3.getIssueCount());
}
)
.verifyComplete();
}
}
결과를 보면 MockWebServer가 띄워진 port로 요청을 보내고 미리 넣어둔 데이터가 Response 된 것을 볼 수 있다.
MockWebServer 뿐만 아니라 Mockito를 사용하여 테스트 할 수도 있지만, 아래와 같이 데이터들을 stubbing하는데 많은 번거로움이 존재한다. MockWebServer가 사용하기 더 편리하고 깔끔하기 때문에 사용을 권장한다.
그리고 https://github.com/spring-projects/spring-framework/issues/19852 이슈를 보면 스프링팀도 MockWebServer를 사용하여 테스트를 하고 있다.
개인적인 사용의 어려움
아래와 같이 SpringBean으로 Handler와 WebClient를 주입받아서 사용하려는 경우에는 MockWebServer를 사용하는데 어려움이 있었다. 실제 코드를 짤때는 예제 코드 처럼 심플한 경우 보다는 여러 가지 Spring의 기술들이 들어간 경우가 많아서 Spring으로 Bean을 주입 받아서 테스트 코드를 짜야 하는 경우들이 있다. WebClient Bean을 생성할 때 url 정보를 입력해 줘야되서 MockWebServer를 생성해서 url을 만들어야 했다.
그러나 이러한 경우에는 MockWebServer를 재사용하게 되어서 테스트 코드 순서를 보장하지 않는 경우에는 안에 데이터가 꼬이게 되는 문제가 발생하였다. 그래서 꼭 재사용하게 된다면 테스트 순서를 보장하도록 어노테이션을 사용하거나 (@TestMethodOrder, @Order) 한번의 요청만 하도록 작성하였다.
개인적으로는 재사용할 수 있도록 clear()라는 메서드가 있으면 더 좋겠다는 생각이 들었다.
@TestConfiguration
public class TestWebClientConfig {
Logger log = LoggerFactory.getLogger(TestWebClientConfig.class);
public static MockWebServer mockWebServer = new MockWebServer();
@Bean
public WebClient testWebClient() {
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1500)
.doOnConnected(conn ->
conn.addHandlerLast(new ReadTimeoutHandler(1500, TimeUnit.MILLISECONDS))
.addHandlerLast(new WriteTimeoutHandler(1500, TimeUnit.MILLISECONDS)));
return WebClient.builder()
.filters(functions -> {
functions.add(logRequest());
functions.add(logResponse());
})
.clientConnector(new ReactorClientHttpConnector(httpClient))
.baseUrl(mockWebServer.url("/github-test").toString())
.build();
}
public ExchangeFilterFunction logRequest() {
return (clientRequest, next) -> {
log.info("Request: {} {}", clientRequest.method(), clientRequest.url());
clientRequest.headers()
.forEach((name, values) -> values.forEach(value -> log.info("{}={}", name, value)));
return next.exchange(clientRequest);
};
}
public ExchangeFilterFunction logResponse() {
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
log.info("Response: {}", clientResponse.headers().asHttpHeaders().get("property-header"));
return Mono.just(clientResponse);
});
}
}
@Import(TestWebClientConfig.class)
@SpringBootTest(classes = TestHandler.class)
class TestHandlerTestSpringBean {
@Autowired
TestHandler handler;
ObjectMapper objectMapper = new ObjectMapper();
@DisplayName("webMockServer Test")
@Test
public void test() {
//given
mockWebServer.enqueue(
new MockResponse().setResponseCode(400)
);
//when
StepVerifier.create(handler.clientDemo6("055055"))
.expectError();
}
@DisplayName("webMockServer Test- 200")
@Test
public void test_200() throws JsonProcessingException {
//given
GitHubDto dto1 = new GitHubDto();
dto1.setId("1622941913451");
dto1.setFullName("055055/bank");
dto1.setIssueCount(0);
GitHubDto dto2 = new GitHubDto();
dto2.setId("1600580264324");
dto2.setFullName("055055/market");
dto2.setIssueCount(0);
GitHubDto dto3 = new GitHubDto();
dto3.setId("444423408312312");
dto3.setFullName("055055/user");
dto3.setIssueCount(0);
List<GitHubDto> gitHubDtoList = Arrays.asList(dto1, dto2, dto3);
String response = objectMapper.writeValueAsString(gitHubDtoList);
mockWebServer.enqueue(
new MockResponse().setBody(response)
.addHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
);
//when
StepVerifier.create(handler.clientDemo6("055055"))
.assertNext(result -> {
assertThat(result.getId()).isEqualTo(dto1.getId());
assertThat(result.getFullName()).isEqualTo(dto1.getFullName());
assertThat(result.getIssueCount()).isEqualTo(dto1.getIssueCount());
}
)
.assertNext(result -> {
assertThat(result.getId()).isEqualTo(dto2.getId());
assertThat(result.getFullName()).isEqualTo(dto2.getFullName());
assertThat(result.getIssueCount()).isEqualTo(dto2.getIssueCount());
}
)
.assertNext(result -> {
assertThat(result.getId()).isEqualTo(dto3.getId());
assertThat(result.getFullName()).isEqualTo(dto3.getFullName());
assertThat(result.getIssueCount()).isEqualTo(dto3.getIssueCount());
}
)
.verifyComplete();
}
MockWebServer For Junit5
MockWebServer를 이용하여 예제를 구현해봤는데 아래와 같이 Junit5/Junit4 를 위한 라이브러리도 있으니 사용해보면 좋을 것 같다.
참고
- Total
- Today
- Yesterday
- update query set multiple
- update set multi
- 슬랙
- update query multi row
- Slack
- 이펙티브자바
- vue.js
- 뱅셀 유전자
- update query
- 이것이 자바다
- spring-boot-starter-data-redis
- MSSQL
- 뱅크샐러드
- 그레이들
- update query mutiple row
- 싱글턴
- 업데이트 쿼리
- springboot https
- multiple row update
- visual studio code
- 다중 업데이트
- SpringBoot
- 뱅크샐러드 유전자
- 몽고DB 완벽가이드
- 슬랙 /
- gradle
- java
- update set multiple
- effectivejava
- 슬랙봇
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |