슬라이스 테스트

​ 슬라이스 테스트는 각 계층에 구현해 놓은 기능들이 잘 동작하는지 특정 계층만 잘라서(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;
    }
}
  1. postMemberTest() : mockMvc.perform() 의 결과를 ResultActions 으로 분리하면 BDD 기법인 given, when, then 으로 구분하기가 편합니다.
  2. header().string("Location", is(startsWith("/v11/members/"))) : ResultActions 의 헤더값을 찾습니다.
  3. getMemberTest() : post("/v11/members") 를 통해 location 정보를 얻은 후 해당 location 으로 다시 요청을 보내 ResultActions 을 얻습니다.
  4. 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();
  1. andExpect() 는 위에서 본 것처럼 ResultMatcher 라는 Matcher 클래스를 통해 검증합니다. 반환값은 그대로 ResultActions 입니다.

  2. andExpectAll() 은 여러 개의 ResultMatcher를 배열로 받아서 순회하며 this.andExpect(matcher) 를 실행시킵니다.

  3. andDo() : 마찬가지로 ResultActions 을 반환하는데요. ResultHandler 이라는 함수형 인터페이스를 실행시킵니다. 스트림의 forEach() 와 비슷하다고 볼 수 있습니다.

    getActions.andDo(result -> {
                String contentAsString = result.getResponse().getContentAsString();
                System.out.println("contentAsString = " + contentAsString);
            });
    
  4. 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) 를 호출합니다. savedMembermember 의 값을 비교합니다.
  • findAllMemberTest() : memberRepository.findAll() 를 호출하고 크기를 비교합니다.
    • assertThat(members).hasSize(2) : list 의 사이즈를 검증합니다.
    • extracting(att1, att2 ...).containsExactlyInAnyOrder(tuple(...), tuple(...)) : members 에서 속성값으로 튜플을 추출하서 비교합니다.

데이터 액세스 계층은 테스트할 내용이 복잡하지는 않습니다. 다만 데이터에 접근하는 로직이 많을 수 있는데, 해당 메서드마다 테스트 작성이 필요할 것입니다.

댓글남기기