슬라이스 테스트 - API 계층

jungseo·2023년 6월 29일
0

Spring

목록 보기
14/23

API 계층 슬라이스 테스트

  • Controller 테스트를 위한 테스트 클래스의 기본 구조
@SpringBootTest       // (1) 
@AutoConfigureMockMvc  // (2)
public class ControllerTestDefaultStructure {
		// (3)
    @Autowired
    private MockMvc mockMvc;
    
		// (4) 
    @Test
    public void postMemberTest() {
        // given (5) 테스트용 request body 생성
        
        // when (6) MockMvc 객체로 테스트 대상 Controller 호출
        
        // then (7) Controller 핸들러 메서드에서 응답으로 수신한 HTTP Status 및 response body 검증 
    }
}
  • (1) @SpringBootTest
    • Spring Boot 기반 애플리케이션을 테스트하기 위한 Application Context 생성
    • 모든 빈을 생성해 무거움
    • 이후 @WebMvcTest 사용
      • 웹 계층 전용 슬라이스 테스트에 적합
      • 컨트롤러에 필요한 빈만 직접 등록해서 사용
  • (2) @AutoConfigureMockMvc
    • Controller 테스트를 위한 애플리케이션을 자동 구성
  • (3) MockMvc
    • Tomcat 같은 서버를 실행하지 않고 Spring 기반 애플리케이션의 Controller를 테스트할 수 있는 완벽한 환경을 지원해 주는 일종의 Spring MVC 테스트 프레임워크
    • MockMvc 객체를 통해 작성한 Controller를 호출해 Controller에 대한 테스트를 진행 가능

1. BeforeEach

import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.assertj.core.api.Assertions.*;

@Transactional
@SpringBootTest
@AutoConfigureMockMvc
class MemberControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private Gson gson;

    private MemberDto.Post post;
    private ResultActions postAction;
    private String postLocation;

    @BeforeEach
    void init() throws Exception {
        MemberDto.Post post = new MemberDto.Post("hgd@gmail.com",
                                                        "홍길동",
                                                    "010-1234-5678");

//			(1)
		String content = gson.toJson(post);

//			(2)
        ResultActions actions =
                mockMvc.perform( // (3)
                                    post("/v11/members") // (3-1)
                                        .accept(MediaType.APPLICATION_JSON) // (3-2)
                                        .contentType(MediaType.APPLICATION_JSON) // (3-3)
                                        .content(content) // (3-4)
                                );

//			(4)
        this.post = post;
        this.postAction = actions;
        this.postLocation = actions.andReturn().getResponse().getHeader("Location");
    }
							...
}
  • (1) Gson
    • post 객체를 JSON 포맷으로 변환
    • implementation 'com.google.code.gson:gson' 추가
  • (2) ResultActions

    • mockMvc.perform() 메서드를 통해 수행된 요청의 결과를 담아 활용 및 검증 가능
  • (3) mockMvc

    • perform() 메서드를 호출해 핸들러 메서드에 요청 전송

    • (3-1) HTTP 메서드와 request URL 설정

    • (3-2) 요청 형식을 JSON 형식으로 설정 (request의 Accept 헤더 설정)

    • (3-3) 요청 본문을 JSON 형식으로 설정 (request의 Content-Type 헤더 설정)

    • (3-4) 요청의 본문에 들어갈 데이터를 지정 (request의 body 설정)

  • (4) 각 테스트 케이스에서 사용하기 위한 전역변수 지정

    • 응답 데이터에 접근해 헤더 값을 가져옴

2. postMemberTest 메서드

    @Test
    void postMemberTest() throws Exception {
        postAction // (1)
                .andExpect(status().isCreated()) // (2)
                .andExpect(header().string("Location", is(startsWith("/v11/members/")))); // (3)
    }
  • (1) @BeforeEach가 붙은 메서드에서 mockMvc로 회원을 등록하고 반환된 결과를 담은 전역 변수
  • (2) response status가 201(Created)인지 검증
  • (3) HTTP header에 추가된 Location의 문자열 값이 “/v11/members/”로 시작하는지 검증

3. getMemberTest 메서드

    @Test
    void patchMemberTest() throws Exception {

//			(1)
        long memberId = Long.parseLong(postLocation.substring(postLocation.lastIndexOf("/") + 1));

//        when patch member (2)
        MemberDto.Patch patch = new MemberDto.Patch(
                memberId,
                "홍길동",
                "010-2222-2222"
        );
        String patchContent = gson.toJson(patch);
        ResultActions patchAction =
                mockMvc.perform(
                        patch("/v11/members/" + memberId)
                                .accept(MediaType.APPLICATION_JSON)
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(patchContent)
                );

//        then (3)
        mockMvc.perform(
                get("/v11/members/" + memberId)
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.name").value(patch.getName()))
                .andExpect(jsonPath("$.data.phone").value(patch.getPhone()));
    }
  • (1) 전역 변수로 지정했던 postMember 응답의 Location 헤더값에서 memberId 추출
  • (2) patch request
  • (3) patch request에 대한 검증을 위해 get request 후 검증

4. getMembers 메서드

    @Test
    void getMembersTest() throws Exception {
//        given 회원 1명 더 등록

        MemberDto.Post post2 = new MemberDto.Post(
                "hgd@naver.com",
                "Gildong",
                "010-2222-2222"
        );
        String postContent2 = gson.toJson(post2);

        mockMvc.perform(
                post("/v11/members")
                        .accept(MediaType.APPLICATION_JSON)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(postContent2)
        );

//        when
        int page = 1;
        int size = 20;

        ResultActions getAction =
                mockMvc.perform( // getMembers 요청
                                 get("/v11/members")
                                         .param("page", String.valueOf(page)) // (1)
                                         .param("size", String.valueOf(size))
                                         .accept(MediaType.APPLICATION_JSON));

        getAction // (2)
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.[0].email").value(post2.getEmail()))
                .andExpect(jsonPath("$.data.[1].email").value(post.getEmail()))
                .andExpect(jsonPath("$.data.[0].name").value(post2.getName()))
                .andExpect(jsonPath("$.data.[1].name").value(post.getName()))
                .andExpect(jsonPath("$.data.[0].phone").value(post2.getPhone()))
                .andExpect(jsonPath("$.data.[1].phone").value(post.getPhone()));
    }
  • (1) param() : request에 파라미터를 추가
  • (2) 요청에 대한 응답 데이터 검증
    • jsonPath() 관련 내용은 아래 작성

5. deleteMemberTest 메서드

    @Test
    void deleteMemberTest() throws Exception {

        long memberId = Long.parseLong(postLocation.substring(postLocation.lastIndexOf("/") + 1));

        mockMvc.perform(
                delete("/v11/members/" + memberId))
                .andExpect(status().isNoContent());
    }

사용한 API

1. MockMvc

  • Spring MVC 프레임워크를 실행하지 않고도 컨트롤러를 테스트 가능

  • DispatcherServlet을 직접 실행하지 않고 HTTP 요청을 생성

  • 별도의 서버 설정이나 네트워크 연결 없이 컨트롤러에 대한 테스트를 수행

  • @SpringBootTest와 함께 사용

  • @AutoConfigureMockMvc 사용하여 MockMvc 객체를 자동으로 구성

  • 메서드

    • perform() : 핸들러 메서드에 요청 전송

    • post(),get() 등 : HTTP 메서드와 request URL 설정

    • accept() : 요청 형식을 JSON 형식으로 설정 (request의 Accept 헤더 설정)

    • contentType() : 요청 본문을 JSON 형식으로 설정 (request의 Content-Type 헤더 설정)

    • content() 요청의 본문에 들어갈 데이터를 지정 (request의 body 설정)

2. ResultActions

  • mockMvc.perform() 메서드를 통해 수행된 요청의 결과를 검증하고 조작하는 데 사용되는 객체
  • 메서드 예시
    • andExpect(status().isOk()) : 응답의 상태 코드가 200 OK인지 검증
    • andExpect(content().contentType(MediaType.APPLICATION_JSON)) : 응답의 미디어 타입이 JSON인지 검증
    • andExpect(jsonPath("$.key").value("expectedValue")) : 응답 본문의 JSON 경로에 있는 값이 예상된 값과 일치하는지 검증
    • andReturn() : 실제 응답을 반환

3. MvcResult

  • ResultActions의 andReturn() 메서드를 통해 반환되는 실제 요청과 응답의 상세 정보를 제공하는 객체

  • 메서드

    • getResponse()

      • 요청에 대한 실제 응답 객체인 HttpServletResponse를 반환
      • 응답의 상태 코드, 헤더, 본문 등 접근 가능
    • getRequest()

      • 요청에 대한 실제 요청 객체인 HttpServletRequest를 반환
      • 요청의 URL, 메서드, 파라미터 등 접근 가능
    • getResponse().getContentAsString(): 응답 본문을 문자열 형태로 반환

4. jsonPath

  • JSON 문서의 경로를 사용하여 특정 값을 추출하거나 검증하기 위해 사용되는 라이브러리나 메서드
  • JSON 응답의 특정 필드나 속성 값을 가져오거나 검증하는 데 사용
  • 사용 예시
{
    "store": {
        "book": [
            {
                "category": "reference",
                "author": "Nigel Rees",
                "title": "Sayings of the Century",
                "price": 8.95
            },
            {
                "category": "fiction",
                "author": "Evelyn Waugh",
                "title": "Sword of Honour",
                "price": 12.99
            },
            {
                "category": "fiction",
                "author": "Herman Melville",
                "title": "Moby Dick",
                "isbn": "0-553-21311-3",
                "price": 8.99
            },
            {
                "category": "fiction",
                "author": "J. R. R. Tolkien",
                "title": "The Lord of the Rings",
                "isbn": "0-395-19395-8",
                "price": 22.99
            }
        ],
        "bicycle": {
            "color": "red",
            "price": 19.95
        }
    },
    "expensive": 10
}

JsonPath 예시 출처


MemberControllerTest 클래스

import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.assertj.core.api.Assertions.*;

@Transactional
@SpringBootTest
@AutoConfigureMockMvc
class MemberControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private Gson gson;

    private MemberDto.Post post;
    private ResultActions postAction;
    private String postLocation;

    @BeforeEach
    void init() throws Exception {
        MemberDto.Post post = new MemberDto.Post("hgd@gmail.com",
                                                        "홍길동",
                                                    "010-1234-5678");
        String content = gson.toJson(post);

        ResultActions actions =
                mockMvc.perform(
                                    post("/v11/members")
                                        .accept(MediaType.APPLICATION_JSON)
                                        .contentType(MediaType.APPLICATION_JSON)
                                        .content(content)
                                );

        this.post = post;
        this.postAction = actions;
        this.postLocation = actions.andReturn().getResponse().getHeader("Location");
    }

    @Test
    void postMemberTest() throws Exception {
        postAction
                .andExpect(status().isCreated())
                .andExpect(header().string("Location", is(startsWith("/v11/members/"))));
    }

    @Test
    void getMemberTest() throws Exception {

        long memberId = Long.parseLong(postLocation.substring(postLocation.lastIndexOf("/") + 1));

        mockMvc.perform(
                        get("/v11/members/" + memberId)
                                .accept(MediaType.APPLICATION_JSON)
                )
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.email").value(post.getEmail()))
                .andExpect(jsonPath("$.data.name").value(post.getName()))
                .andExpect(jsonPath("$.data.phone").value(post.getPhone()));
    }

    @Test
    void patchMemberTest() throws Exception {

        long memberId = Long.parseLong(postLocation.substring(postLocation.lastIndexOf("/") + 1));

        MemberDto.Patch patch = new MemberDto.Patch(
                memberId,
                "홍길동",
                "010-2222-2222"
        );
        String patchContent = gson.toJson(patch);
        ResultActions patchAction =
                mockMvc.perform(
                        patch("/v11/members/" + memberId)
                                .accept(MediaType.APPLICATION_JSON)
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(patchContent)
                );

        mockMvc.perform(
                get("/v11/members/" + memberId)
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.name").value(patch.getName()))
                .andExpect(jsonPath("$.data.phone").value(patch.getPhone()));
    }

    @Test
    void getMembersTest() throws Exception {

        MemberDto.Post post2 = new MemberDto.Post(
                "hgd@naver.com",
                "Gildong",
                "010-2222-2222"
        );
        String postContent2 = gson.toJson(post2);

        mockMvc.perform(
                post("/v11/members")
                        .accept(MediaType.APPLICATION_JSON)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(postContent2)
        );

        int page = 1;
        int size = 20;

        ResultActions getAction =
                mockMvc.perform(
                                 get("/v11/members")
                                         .param("page", String.valueOf(page))
                                         .param("size", String.valueOf(size))
                                         .accept(MediaType.APPLICATION_JSON));

        getAction
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.[0].email").value(post2.getEmail()))
                .andExpect(jsonPath("$.data.[1].email").value(post.getEmail()))
                .andExpect(jsonPath("$.data.[0].name").value(post2.getName()))
                .andExpect(jsonPath("$.data.[1].name").value(post.getName()))
                .andExpect(jsonPath("$.data.[0].phone").value(post2.getPhone()))
                .andExpect(jsonPath("$.data.[1].phone").value(post.getPhone()));
    }

    @Test
    void deleteMemberTest() throws Exception {

        long memberId = Long.parseLong(postLocation.substring(postLocation.lastIndexOf("/") + 1));

        mockMvc.perform(
                delete("/v11/members/" + memberId))
                .andExpect(status().isNoContent());
    }
}

0개의 댓글