[Spring] slice 테스트 - API, Data Access Layer 테스트
슬라이스 테스트
슬라이스 테스트는 각 계층에 구현해 놓은 기능들이 잘 동작하는지 특정 계층만 잘라서(Slice) 테스트하는 것입니다.
API 계층 테스트
API 계층의 테스트는 MockMvc
를 사용합니다. DI로 주입받은 MockMvc
는 Tomcat 같은 서버를 실행하지 않고 Spring 기반 애플리케이션의 Controller를 테스트할 수 있는 완벽한 환경을 지원해 주는 일종의 Spring MVC 테스트 프레임워크입니다. 아래와 같은 형식입니다.
@SpringBootTest
@AutoConfigureMockMvc
class MemberControllerTest {
@Autowired private MockMvc mockMvc;
@Autowired private Gson gson;
@MockBean MemberService memberService;
@Autowired MemberMapper mapper;
@Test
void postMemberTest() throws Exception {
// given
MemberDto.Post post = new MemberDto.Post("hgd@gmail.com", "홍길동", "010-1234-5678");
Member member = createMember(1L);
member.setEmail(post.getEmail());
member.setName(post.getName());
member.setPhone(post.getPhone());
when(memberService.createMember(any(Member.class))).thenReturn(member);
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/"))));
}
@Test
void getMemberTest() throws Exception {
Member member = createMember(1L);
when(memberService.findMember(any(Long.class))).thenReturn(member);
// when // then
mockMvc.perform(
get("/v11/members/" + member.getMemberId())
.accept(MediaType.APPLICATION_JSON)
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.email").value(member.getEmail()))
.andExpect(jsonPath("$.data.name").value(member.getName()))
.andExpect(jsonPath("$.data.phone").value(member.getPhone()));
}
private Member createMember(Long i) {
Member member = new Member();
member.setMemberId(i);
member.setEmail("test" + i + "@gmail.com");
member.setName("test" + i);
member.setPhone(i + "-1234-5678");
Stamp stamp = new Stamp();
member.setStamp(stamp);
return member;
}
}
postMemberTest()
:mockMvc.perform()
의 결과를ResultActions
으로 분리하면 BDD 기법인 given, when, then 으로 구분하기가 편합니다.header().string("Location", is(startsWith("/v11/members/")))
:ResultActions
의 헤더값을 찾습니다.getMemberTest()
:post("/v11/members")
를 통해location
정보를 얻은 후 해당location
으로 다시 요청을 보내ResultActions
을 얻습니다.jsonPath("$.data.email").value(post.getEmail())
으로 json 형식의 data 를 분석해서 비교할 수 있습니다.
만약 response body 응답 데이터에 포함된 한글이 깨질 경우
application.yml
에 아래와 같이 설정합니다.server: servlet: encoding: force-response: true
ResultActions
MockMvc.perform(...)
의 결과로 나오는 ResultActions
은 인터페이스로, 다음과 같은 메서드를 가집니다.
public interface ResultActions {
ResultActions andExpect(ResultMatcher matcher) throws Exception;
default ResultActions andExpectAll(ResultMatcher... matchers) throws Exception {
ExceptionCollector exceptionCollector = new ExceptionCollector();
for (ResultMatcher matcher : matchers) {
exceptionCollector.execute(() -> this.andExpect(matcher));
}
exceptionCollector.assertEmpty();
return this;
}
ResultActions andDo(ResultHandler handler) throws Exception;
MvcResult andReturn();
-
andExpect()
는 위에서 본 것처럼ResultMatcher
라는 Matcher 클래스를 통해 검증합니다. 반환값은 그대로ResultActions
입니다. -
andExpectAll()
은 여러 개의ResultMatcher
를 배열로 받아서 순회하며this.andExpect(matcher)
를 실행시킵니다. -
andDo()
: 마찬가지로ResultActions
을 반환하는데요.ResultHandler
이라는 함수형 인터페이스를 실행시킵니다. 스트림의forEach()
와 비슷하다고 볼 수 있습니다.getActions.andDo(result -> { String contentAsString = result.getResponse().getContentAsString(); System.out.println("contentAsString = " + contentAsString); });
-
andReturn()
:MvcResult
를 리턴합니다.ResultActions
의 결과값을 직접 받아보고 싶을 때, 리턴값으로 사용합니다.
MvcResult
이제 ResultActions.andReturn()
의 결과값인 MvcResult
을 보겠습니다.
public interface MvcResult {
MockHttpServletRequest getRequest();
MockHttpServletResponse getResponse();
@Nullable
Object getHandler();
@Nullable
HandlerInterceptor[] getInterceptors();
@Nullable
ModelAndView getModelAndView();
@Nullable
Exception getResolvedException();
FlashMap getFlashMap();
Object getAsyncResult();
Object getAsyncResult(long timeToWait);
}
getRequest()
:MockHttpServletRequest
객체를 반환합니다. 이 객체는 요청에 대한 정보를 제공하고 테스트에서 조작할 수 있습니다.getResponse()
:MockHttpServletResponse
객체를 반환합니다. 이 객체는 응답에 대한 정보를 제공하고 테스트에서 조작할 수 있습니다.getHandler()
: 처리되는 요청 핸들러(컨트롤러 메서드) 객체를 반환합니다.getInterceptors()
: 요청에 적용된HandlerInterceptor
배열을 반환합니다.getModelAndView()
: 컨트롤러 메서드에서 반환된ModelAndView
객체를 반환합니다. 이 객체는 모델 데이터와 뷰 이름을 포함합니다.getResolvedException()
: 처리된 예외 객체를 반환합니다.getFlashMap()
:FlashMap
객체를 반환합니다. 이 객체는 리다이렉션 요청 시 플래시 매개변수를 저장하는 데 사용됩니다.getAsyncResult()
: 비동기 요청의 결과 객체를 반환합니다.getAsyncResult(long timeToWait)
: 주어진 대기 시간(timeToWait
) 동안 비동기 요청의 결과 객체를 반환합니다.
아무래도 가장 많이 사용하는 메서드는 getRequest()
와 getResponse()
이 되겠죠? 두 메서드 모두 .getContentAsString()
로 내용을 String
으로 받을 수 있습니다. Gson
이나 ObjectMapper
를 사용해서 해당 객체로 변환할 수도 있습니다.
헤더는 .getHeader("헤더이름")
을 하면 String
으로 반환받을 수 있습니다.
String contentAsString = postActions.andReturn().getRequest().getContentAsString();
String location = postActions.andReturn().getResponse().getHeader("Location"); // "/v11/members/1"
데이터 액세스 계층 테스트
데이터 액세스 계층은 DB 와 데이터 액세스 계층을 함께 테스트하는 통합 테스트입니다. 다만 부분적으로는 각 메서드의 실행을 보는 단위 테스트의 성격도 가지고 있습니다.
데이터 액세스 계층에서 중요한 건 데이터 조작 후 트랜잭션이 롤백되어 각 테스트가 독립적으로 실행되야 한다는 점입니다. 이 부분은 테스트에서 트랜잭션이 끝난 후 자동으로 롤백을 하기 때문에 문제가 없습니다. 만약 데이터를 롤백시키고 싶지 않으면 @Rollback(false)
나 @Commit
어노테이션을 붙입니다.
아래는 간단한 데이터 액세스 계층 테스트 예시입니다.
@DataJpaTest
class MemberRepositoryTest {
@Autowired
private MemberRepository memberRepository;
@Test
public void saveMemberTest() {
// given
Member member = new Member();
member.setEmail("hgd@gmail.com");
member.setName("홍길동");
member.setPhone("010-1111-2222");
// when
Member savedMember = memberRepository.save(member);
// then
assertNotNull(savedMember);
assertTrue(member.getEmail().equals(savedMember.getEmail()));
assertTrue(member.getName().equals(savedMember.getName()));
assertTrue(member.getPhone().equals(savedMember.getPhone()));
}
@Test
public void findAllMemberTest() {
// given
Member member = new Member();
member.setEmail("hgd@gmail.com");
member.setName("홍길동");
member.setPhone("010-1111-2222");
memberRepository.save(member);
// when
List<Member> members = memberRepository.findAll();
// then
assertThat(members)
.hasSize(2)
.extracting("email", "name", "phone")
.containsExactlyInAnyOrder(
tuple("test1@gmail.com", "test1", "010-1111-1111"),
tuple("test2@gmail.com", "test2", "010-2222-2222")
);
}
}
@DataJpaTest
: 데이터 액세스 계층 테스트를 위한 어노테이션입니다.@SpringBootTest
보다 가볍습니다.saveMemberTest()
:member
를 저장한 후memberRepository.save(member)
를 호출합니다.savedMember
와member
의 값을 비교합니다.findAllMemberTest()
:memberRepository.findAll()
를 호출하고 크기를 비교합니다.assertThat(members).hasSize(2)
:list
의 사이즈를 검증합니다.extracting(att1, att2 ...).containsExactlyInAnyOrder(tuple(...), tuple(...))
:members
에서 속성값으로 튜플을 추출하서 비교합니다.
데이터 액세스 계층은 테스트할 내용이 복잡하지는 않습니다. 다만 데이터에 접근하는 로직이 많을 수 있는데, 해당 메서드마다 테스트 작성이 필요할 것입니다.
댓글남기기