@SpringBootTest
@AutoConfigureMockMvc
와 함께 사용된다.@AutoConfigureMockMvc
@WebMvcTest
@AutoConfigureMockMvc
같은 자동 구성이 없다.✅
@AutoConfigureMockMvc
과@WebMvcTest
은 서블릿 컨테이너를 모킹하는 역할을 한다.
➡️ 해당 애너테이션을 통해 사용할 수 있는 것이 MockMvc 객체(서블릿 컨테이너의 Mock 객체)
MockMvc mockMvc
❗️ 웹 환경에서 Controller를 테스트하려면 반드시 서블릿 컨테이너가 구동되고 DispatcherServlet 객체가 메모리에 올라가야 하지만, 서블릿 컨테이너를 Mocking하면 실제 서블릿 컨테이너가 아닌 테스트용 모형 컨테이너를 사용하기 때문에 간단하게 Controller를 테스트할 수 있다.
💡
@SpringBootTest
과@WebMvcTest
는 언제 사용해야 하나요?
-@SpringBootTest
: 다른 계층들과 연동되는 통합 테스트에서 주로 사용
-@WebMvcTest
: Controller를 위한 슬라이스 테스트에 주로 사용
@SpringBootTest
@AutoConfigureMockMvc
class MemberControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private Gson gson;
@Test
void postMemberTest() throws Exception {
// given
MemberDto.Post post = new MemberDto.Post("hgd@gmail.com", "홍길동", "010-1234-5678");
String content = gson.toJson(post);
// when
ResultActions actions =
mockMvc.perform(post("/v11/members")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(content)
);
// then
actions
.andExpect(status().isCreated())
.andExpect(header().string("Location", is(startsWith("/v11/members/"))));
}
}
perform()
: 핸들러 메서드에 요청을 전송post("/v11/members")
contentType(MediaType.APPLICATION_JSON)
accept(MediaType.APPLICATION_JSON)
content(content)
URI getUri = UriComponentsBuilder.newInstance().path("/v11/members/{member-id}").buildAndExpand(memberId).toUri();
URI를 직접 작성해줄 필요없이, UriComponentsBuilder를 통해 생성할 수 있다.
https://blog.naver.com/PostView.naver?blogId=aservmz&logNo=222322019981
.andExpect(jsonPath("$.data.email").value(post.getEmail()));
// json data
{"data":{"memberId":1,"email":"chaeeun@naver.com","name":"홍길동","phone":"010-7777-5555","memberStatus":"활동중","stamp":0}}
❗️ 주의할 점
1. 항상 DB를 테스트 케이스 실행 이전의 상태로 되돌려놓는다.
-> 테스트 케이스 종료 시점에 저장한 데이터를 삭제하기!
2. 테스트 케이스는 순서가 없다.
-> 각 테스트 케이스는 독립적이여야 한다.
@DataJpaTest
: Repository의 기능을 사용하기 위한 자동 구성 작업을 해준다.@DataJpaTest
은 @Transactional
도 포함하고 있기 때문에 테스트 케이스 종료 시점에 항상 rollback 처리된다.Controller 테스트 코드를 보면, 모두 중복되는 코드도 많고 캡슐화가 잘 되지 않았다는 느낌이 든다.
Controller의 테스트 코드를 리팩토링해보자.
1. HTTP request 정보 설정을 위한 interface 생성
테스트 케이스마다 HTTP request를 매번 작성해줬다.
이를 떼어내서 ControllerTestHelper 인터페이스로 생성하였다.
✔️ 구성
MockMvcRequestBuilders 클래스
사용public interface ControllerTestHelper {
default RequestBuilder postRequestBuilder(URI uri, String content) {
return MockMvcRequestBuilders
.post(uri)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(content);
}
default RequestBuilder patchRequestBuilder(URI uri, String content) {
return MockMvcRequestBuilders
.patch(uri)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(content);
}
default RequestBuilder getRequestBuilder(URI uri) {
return MockMvcRequestBuilders
.get(uri)
.accept(MediaType.APPLICATION_JSON);
}
default RequestBuilder getRequestBuilder(URI uri, MultiValueMap<String, String> queryParams) {
return MockMvcRequestBuilders
.get(uri)
.params(
queryParams
)
.accept(MediaType.APPLICATION_JSON);
}
default RequestBuilder deleteRequestBuilder(URI uri) {
return MockMvcRequestBuilders.delete(uri);
}
default URI createURI(String url) {
return UriComponentsBuilder.newInstance().path(url).build().toUri();
}
default URI createURI(String url, long resourceId) {
return UriComponentsBuilder.newInstance().path(url).buildAndExpand(resourceId).toUri();
}
}
2. 핸들러 메서드마다 매번 Data를 생성하지 않고, StubData를 생성해두고 요청에 따라 가져와서 사용하도록 수정한다.
public class StubData {
private static Map<HttpMethod, Object> stubMemberDto;
static {
stubMemberDto = new HashMap<>();
stubMemberDto.put(HttpMethod.POST, new MemberDto.Post("hgd@gmail.com","홍길동",
"010-1111-1111"));
stubMemberDto.put(HttpMethod.PATCH, new MemberDto.Patch(1, null, "010-2222-2222", null));
}
public static class MockMember {
public static Object get(HttpMethod method) {
return stubMemberDto.get(method);
}
}
}
3. 핸들러 메서드마다 중복되는 코드를 init() 함수로 빼준다.
@BeforeEach
를 추가했기 때문에, 각 테스트 케이스마다 실행된다.public class MemberControllerTest implements MemberControllerTestHelper {
private ResultActions postResultActions;
private MemberDto.Post post;
@BeforeEach
public void init() throws Exception {
this.post = (MemberDto.Post) StubData.MockMember.get(HttpMethod.POST);
String content = gson.toJson(post);
URI uri = getURI();
this.postResultActions = mockMvc.perform(postRequestBuilder(uri, content));
}
}
Q1) contentType, accept에 MediaType.APPLICATION_JSON
을 항상 작성해야 하나요?
Spring Boot에서 테스트 할 때 @RestController를 추가한 컨트롤러는 MediaType으로 JSON을 디폴트로 인식을 하는 것 같다.
post, patch, get에서 accept와 contentType를 지워봤는데, accept를 지웠을 때는 모두 passed 되었지만 contentType을 지우니 HttpMediaTypeNotSupportedException
이 발생했다.
❗️ contentType은 꼭 작성해줄 것!