테스팅(Testing) - 슬라이스 테스트

박채은·2023년 1월 5일
0

Spring

목록 보기
23/35

슬라이스 테스트

  • 특정 계층만 잘라서 테스트하는 것
  • Mockito를 통해 Mock 객체를 사용하여 계층 간의 연결을 끊어주지 않으면 완전한 슬라이스 테스트라고 할 수 없다.

API 계층

Controller 테스트

  • @SpringBootTest
    • Spring Boot 기반의 애플리케이션을 테스트하기 위한 Application Context를 생성한다.
    • 프로젝트에서 사용하는 전체 빈을 불러와서 Application Context에 등록한다.
      => 상대적으로 무겁고 느리다.
    • @AutoConfigureMockMvc와 함께 사용된다.
  • @AutoConfigureMockMvc
    • 애플리케이션의 자동 구성 작업을 해준다.
  • @WebMvcTest
    • Controller를 테스트 하기 위한 전용 애너테이션
    • Controller 테스트에 필요한 빈(클래스)만 Application Context에 등록한다.
      => 상대적으로 가볍고 빠르다.
    • 하지만, 필요한 빈들을 수동으로 등록해야 한다.
    • @AutoConfigureMockMvc 같은 자동 구성이 없다.
    • Controller에 의존하고 있는 객체가 있다면, 해당 객체를 Mock 객체를 통해 의존성을 제거해야 한다.

@AutoConfigureMockMvc@WebMvcTest은 서블릿 컨테이너를 모킹하는 역할을 한다.
➡️ 해당 애너테이션을 통해 사용할 수 있는 것이 MockMvc 객체(서블릿 컨테이너의 Mock 객체)


  • MockMvc mockMvc
    • 아파치 톰캣 같은 서블릿 컨테이너를 대신하는 가짜(서블릿 컨테이너를 Mocking)
    • 서블릿 컨테이너(Tomcat) 없이 DispatcherServlet을 호출하는 역할!
    • Spring MVC 테스트 프레임워크
    • DI로 주입을 받아야 함(@Autowired)

❗️ 웹 환경에서 Controller를 테스트하려면 반드시 서블릿 컨테이너가 구동되고 DispatcherServlet 객체가 메모리에 올라가야 하지만, 서블릿 컨테이너를 Mocking하면 실제 서블릿 컨테이너가 아닌 테스트용 모형 컨테이너를 사용하기 때문에 간단하게 Controller를 테스트할 수 있다.

💡 @SpringBootTest@WebMvcTest는 언제 사용해야 하나요?
- @SpringBootTest : 다른 계층들과 연동되는 통합 테스트에서 주로 사용
- @WebMvcTest : Controller를 위한 슬라이스 테스트에 주로 사용


PostTest 코드 (Mockito 사용 x)

@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/"))));
    }
}

1) given

  • request body 생성

2) when

  • perform() : 핸들러 메서드에 요청을 전송
    • perform() 메서드 내부에 핸들러 메서드 호출을 위한 세부적인 정보들(HTTP request)을 작성한다.
    • MockMvcRequestBuilders 클래스를 통해서 request 정보를 전달한다.
  • HTTP 메서드와 URI를 지정
    => post("/v11/members")
  • request body Type과 response body Type를 지정
    • request body Type - contentType(MediaType.APPLICATION_JSON)
    • response body Type - accept(MediaType.APPLICATION_JSON)
  • request body를 포함
    => content(content)

3) then

  • HTTP status와 response body를 받아 검증한다.
  • andExpect() 메서드 사용

URI 객체

URI getUri = UriComponentsBuilder.newInstance().path("/v11/members/{member-id}").buildAndExpand(memberId).toUri();

URI를 직접 작성해줄 필요없이, UriComponentsBuilder를 통해 생성할 수 있다.

https://blog.naver.com/PostView.naver?blogId=aservmz&logNo=222322019981

jsonPath()

.andExpect(jsonPath("$.data.email").value(post.getEmail()));

// json data
{"data":{"memberId":1,"email":"chaeeun@naver.com","name":"홍길동","phone":"010-7777-5555","memberStatus":"활동중","stamp":0}}
  • $ : 최상위 위치
  • jsonPath()를 통해 각 프로터피의 값을 가져올 수 있다.

데이터 액세스 계층

❗️ 주의할 점
1. 항상 DB를 테스트 케이스 실행 이전의 상태로 되돌려놓는다.
-> 테스트 케이스 종료 시점에 저장한 데이터를 삭제하기!

2. 테스트 케이스는 순서가 없다.
-> 각 테스트 케이스는 독립적이여야 한다.

Repository 테스트

  • @DataJpaTest: Repository의 기능을 사용하기 위한 자동 구성 작업을 해준다.
  • @DataJpaTest@Transactional도 포함하고 있기 때문에 테스트 케이스 종료 시점에 항상 rollback 처리된다.

Controller 테스트 리팩토링

Controller 테스트 코드를 보면, 모두 중복되는 코드도 많고 캡슐화가 잘 되지 않았다는 느낌이 든다.
Controller의 테스트 코드를 리팩토링해보자.


1. HTTP request 정보 설정을 위한 interface 생성

테스트 케이스마다 HTTP request를 매번 작성해줬다.
이를 떼어내서 ControllerTestHelper 인터페이스로 생성하였다.


✔️ 구성

  • perform() 메서드 내부에 들어갈 request 정보를 생성해주는 메서드
    • MockMvcRequestBuilders 클래스 사용
  • URI 생성해주는 메서드
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를 생성해두고 요청에 따라 가져와서 사용하도록 수정한다.

  • Map에 데이터를 저장한다.
    • key는 HttpMethod, value는 Object(data)로 저장한다.
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() 함수로 빼준다.

  • 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을 디폴트로 인식을 하는 것 같다.

https://stackoverflow.com/questions/35123835/spring-requestmapping-for-controllers-that-produce-and-consume-json

post, patch, get에서 accept와 contentType를 지워봤는데, accept를 지웠을 때는 모두 passed 되었지만 contentType을 지우니 HttpMediaTypeNotSupportedException이 발생했다.

❗️ contentType은 꼭 작성해줄 것!

0개의 댓글