[첫번째 프로젝트] 9. MockMvc

DAEILLIM·2024년 1월 22일
0

첫번째 프로젝트

목록 보기
9/17
post-thumbnail

MockMvc

1. MockMvc

  • MockMvc는 스프링 프레임워크에서 제공하는 웹 애플리케이션 테스트용 라이브러리이다.
  • MockMvc의 핵심은 웹 애플리케이션의 다양한 컴포넌트를 테스트 수행이다.

MockMvc를 사용하면 HTTP 요청을 작성하고 컨트롤러의 응답을 검증할 수 있습니다. 이를 통해 통합 테스트를 실행하지 않고도 컨트롤러의 동작을 확인할 수 있습니다.

1.1 MockMvc를 이용한 테스트 목적

MockMvc를 이용하여 컨트롤러의 동작을 테스트하는 데 사용된다. 좀 더 자세하게 설명하자면, 컨트롤러의 엔드포인트를 호출하여 HTTP 클라이언트의 요청을 모방하고 적절한 응답을 확인하기 위해 테스트를 수행하는 방식이다. 이러한 테스트 과정을 통해 애플리케이션의 서비스 로직이나 API 엔드포인트가 의도한 대로 동작하는지 확인하고, 버그를 발견하고 수정하는 데 큰 도움을 준다.

1.2 MockMvc를 이용한 Controller내의 흐름

  1. 테스트 코드 → MockMvc

    • 테스트 코드에서 MockMvc 객체를 생성한다. MockMvc 객체는 테스트할 컨트롤러와 상호작용하게 된다.
  2. MockMvcTestDispatcher Servlet

    • MockMvc를 사용하여 원하는 엔드포인트에 요청을 보낸다. 또한 해당 요청에 필요한 파라미터, 헤더, 쿠키 등을 설정할 수 있다.
    • 예를 들어, GET 요청을 보내고 싶다면 perform(MockMvcRequestBuilders.get("/endpoint"))와 같이 요청을 설정하면 된다.
    • 파라미터 설정은 param("paramName", "paramValue")와 같이 파라미터를 작성한다.
  3. TestDispatcher ServletController

    • 요청을 실행하고 응답을 받는다. andExpect 메서드를 사용하여 응답의 상태코드, 헤더, 본문 등을 검증을 수행한다.
  4. MockMvc → 테스트 코드

    • 필요한 검증을 추가한다.
      예를 들어, 응답 본문의 내용을 검증하고 싶다면 andExpect(content(). string("expectedValue"))와 같이 검증을 추가한다.

1.3 MockMvc 동작

MockMvc테스트 실행 시 요청부터 응답까지 의 처리 흐름은 다음 그림과 같다.

image-20240122194400447
구분설명
(1)테스트 메서드는 Spring Test에서 제공하는
org.springframework.test.web.servlet.TestDispatcherServlet에서 요청할 데이터를 설정합니다.
(2)MockMvcTestDispatcherServlet에 의사 요청을 보냅니다.
(3)TestDispatcherServlet에서 Controller가 요청 세부 정보를 일치시키는 메서드를 호출합니다.
(4)테스트 메서드는 MockMvc에서 실행 결과를 받아 실행 결과의 유효성을 확인합니다.

MockMvc 에 대해 정리하자면 다음과 같다.

  • MockMvc는 스프링 프레임워크에서 제공하는 테스트용 라이브러리이다.
  • MockMvc 라이브러리를 사용하면 Spring MVC 컨트롤러의 단위 테스트를 쉽게 작성할 수 있다.
  • MockMvc를 사용하면 HTTP 요청을 작성하고 컨트롤러의 응답을 검증할 수 있다.
  • 통합 테스트를 실행하지 않고도 컨트롤러의 동작을 확인할 수 있다.

1.4 MockMvc 메서드

메서드설명
standaloneSetup()특정 컨트롤러를 MockMvc에 설정하여
테스트할 수 있는 환경을 구성
perform()MockMvc를 사용하여 HTTP 요청을 실행
andExpect()컨트롤러의 응답을 검증
andExpect(status().isOk())응답 상태 코드가 200인지 확인
andExpect(content().string("expected"))응답 본문의 내용이 "expected"인지 확인
andExpect(jsonPath("$.property").value("expected"))JSON 응답에서 특정 속성의 값이 "expected"인지 확인
andExpect(view().name("expectedView"))응답에 대한 뷰의 이름이 "expectedView"인지 확인
andExpect(model().attribute("attributeName", "expectedValue"))모델 속성의 값이 "expectedValue"인지 확인
andExpect(redirectedUrl("expectedUrl"))리다이렉트된 URL이 "expectedUrl"인지 확인

1.5 MockMvc 예제1

@ExtendWith(MockitoExtension.class) // Junit5 - Mockito 연동
class MockMvcControllerTest {
    private MockMvc mockMvc;   // HTTP 호출을 위한 MockMVC 사용
    
    @BeforeEach
    void setUp() {
        mockMvc = MockMvcBuilders
            .standaloneSetup(codeController)
            .build();
    }

    @Test
    public void testExample() throws Exception {
        mckMvc.perfrom(get("/example"))
            .andExpect(status().isOk())
            .andExpect(content().string("expected"))
            .andExpect(jsonPath("$.property").value("expected")) 
            .andExpect(view().name("expectedView")) 
            .andExpect(model().attribute("attributeName", "expectedValue"))
            .andExpect(redirectedUrl("expectedUrl"));
    }
// 1. "/example"로 get 요청
mckMvc.perfrom(get("/example"))
// 2. 상태 코드 200 응답 확인
.andExpect(status().isOk())
// 3. 응답 내용이 "expected"인지 확인
.andExpect(content().string("expected"))
// 4. JSON의 "property" 속성값이 "expected"와 동일한지 확인
.andExpect(jsonPath("$.property").value("expected")) 
// 5. 뷰 이름이 "expectedView"인지 확인
.andExpect(view().name("expectedView")) 
// 6. 모델의 "attributeName" 속성값이 "expectedValue"인지 확인
.andExpect(model().attribute("attributeName", "expectedValue"))
 // 7. "expectedUrl"으로 리다이렉트인지 확인
.andExpect(redirectedUrl("expectedUrl"));

1.6 MockMvc 예제2(중요)

    @DisplayName("게시글 목록 조회하기")
    @Test
    public void Given_Nothing_When_RequestListOfArticle_Then_ReturnListOfArticle() throws Exception {
        // When + Then
        mvc.perform(get("/articles"))
                .andExpect(status().isOk())
                .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML))
                .andExpect(model().attributeExists("articles"));
    }

게시글 목록 조회하는 테스트 코드에서 andExpect 메서드의 일부 코드가 변경되었다. 구체적으로 다음의 코드가 변경된다. andExpect(content().contentType(...)에서 andExpect(content().contentTypeCompatibleWith(...)) 으로 변경해야 한다. 변경한 이유에 대해 알기 위해서 각 메서드 의미를 충분히 학습한다.

andExpect(content().contentType(...))andExpect(content().contentTypeCompatibleWith(...))는 두 가지 다른 메서드입니다. 이 두 메서드는 각각 다른 컨텐츠 타입 검증 기능을 제공합니다.

  1. andExpect(content().contentType(...)):

    • 이 메서드는 정확한 컨텐츠 타입을 기대하고 검증합니다. 즉, 예상되는 컨텐츠 타입과 정확하게 일치해야 합니다.
    • 예를 들어, andExpect(content().contentType(MediaType.APPLICATION_JSON))는 HTTP 응답의 컨텐츠 타입이 JSON인지를 확인합니다.
    mockMvc.perform(MockMvcRequestBuilders.get("/api/data"))
            .andExpect(MockMvcResultMatchers.content()
                       .contentType(MediaType.APPLICATION_JSON));
  2. andExpect(content().contentTypeCompatibleWith(...)):

    • 이 메서드는 주어진 컨텐츠 타입 또는 해당 타입과 호환되는지를 검증합니다. 즉, 주어진 타입과 호환되면 검증을 통과합니다.
    • 예를 들어, andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))는 HTTP 응답의 컨텐츠 타입이 JSON 또는 JSON과 호환되는지를 확인합니다.
    emockMvc.perform(MockMvcRequestBuilders.get("/api/data"))
            .andExpect(MockMvcResultMatchers.content()
                       .contentTypeCompatibleWith(MediaType.APPLICATION_JSON));

이 두 메서드의 선택은 테스트 시나리오 및 검증의 목적에 따라 다릅니다. 대부분의 경우에는 contentTypeCompatibleWith를 사용하여 더 유연한 검증을 수행할 수 있습니다. 특히, 컨텐츠 타입이 정확하게 일치하지 않더라도 호환되는 경우에 테스트를 통과하도록 할 때 유용합니다.

간단히 요약하자면, contentType()는 엄격한 컨텐츠 타입을 검증합니다.
반면에 contentTypeCompatibleWith()는 컨텐츠 타입이 호환되는 경우에도 검증에 성공합니다.

2. ResultActions

2.1 ResultActions 메서드

메서드설명
andReturn()해결된 MvcResult 객체를 반환
andReturn(MvcResult)반환할 MvcResult를 설정
andDo(ResultHandler)결과에 대해 추가 작업을 수행
andDo(ResultMatcher)결과에 ResultMatcher를 추가
andExpect(ResultMatcher)결과에 대한 기대치로 ResultMatcher를 추가
andForward()요청을 다음 핸들러로 전달
andForward(String)요청을 지정된 URL로 전달
andExpect(MockMvcResultMatchers)MockMvcResultMatchers에서 ResultMatcher를 추가
andExpect(MockMvcResultHandlers)MockMvcResultHandlers에서 ResultHandler를 추가
andReverse()이전 전달을 뒤집음
andForwardDefault()요청을 기본 핸들러로 전달
andForwardDefault(String)요청을 지정된 URL로 기본 핸들러로 전달
andReturnDefault()기본 핸들러에 대한 해결된 MvcResult 객체를 반환
andReturnDefault(MvcResult)기본 핸들러에 반환할 MvcResult를 설정
andDoDefault()기본 핸들러에 대해 추가 작업을 수행
andDoDefault(ResultHandler)지정된 핸들러를 사용하여 기본 핸들러에 대해 추가 작업을 수행
andDoDefault(ResultMatcher)기본 핸들러에 ResultMatcher를 추가
andExpectDefault(ResultMatcher)기본 핸들러에 대한 결과 기대치로 ResultMatcher를 추가
andForwardNamed(String)지정된 URL로 명명된 핸들러로 요청을 전달
andReturnNamed(String)명명된 핸들러에 대한 해결된 MvcResult 객체를 반환
andDoNamed(String)명명된 핸들러에서 추가 작업을 수행
andDoNamed(String, ResultHandler)지정된 핸들러를 사용하여 명명된 핸들러에서 추가 작업을 수행
andDoNamed(String, ResultMatcher)명명된 핸들러에 ResultMatcher를 추가
andExpectNamed(String, ResultMatcher)명명된 핸들러에 대한 결과 기대치로 ResultMatcher를 추가
andReverseNamed(String)이전 명명된 핸들러로의 전달을 뒤집음
andReverseDefault()기본 핸들러로의 이전 전달을 뒤집음

2.2 ResultActions 예제1

public class SampleTest {
    private MockMvc mockMvc;
    // ...
    public void sampleTest() throws Exception {
        ResultActions resultActions = mockMvc
            .perform(MockMvcRequestBuilders.get("/api/sample"));
        
        resultActions
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.message").value("Hello, World!"));
        // Add more assertions or actions as needed
    }
}
  • SampleTest 클래스는 테스트를 수행하는 메서드인 sampleTest()를 가지고 있다.
  • 이 메서드에서는 /api/sample 엔드포인트에 대한 GET 요청을 수행하고, 응답의 상태 코드가 200 (OK)이며, JSON 응답의 message 필드가 "Hello, World!"인지를 검증한다.

3. MvcResult

3.1 MvcResult 메서드

메서드설명
getModelAndView()응답에 대한 모델과 뷰를 담고 있는 ModelAndView 객체를 반환
getRequest()응답과 관련된 HttpServletRequest 객체를 반환
getResponse()응답과 관련된 HttpServletResponse 객체를 반환
getResponseHeaders()응답 헤더를 맵 형태로 반환
getResponseStatus()응답 상태 코드를 반환
getModel()응답으로부터 모델 객체를 반환
getModelAndViewName()ModelAndView 객체로부터 뷰의 이름을 반환

3.2 MvcResult 예제1

// ModelAndView 객체에서 모델을 가져온다.
mvcResult.getModelAndView().getModel(); 
// ModelAndView 객체의 이름을 가져온다.
mvcResult.getModelAndView().getModelAndViewName(); 
mvcResult.getRequest(); // 요청 객체를 가져온다.
mvcResult.getResponse(); // 응답 객체를 가져온다.
mvcResult.getResponseHeaders(); // 응답 헤더를 가져온다.
mvcResult.getResponseStatus(); // 응답 상태를 가져온다.

4. MockMvc에서 사용되는 어노테이션

MockMvc의 어노테이션은 JUnit5 어노테이션과 Mockito 어노테이션을 포함하였으며 테스트를 진행할 때 주로 사용되는 어노테이션 위주로 구성되었습니다.

어노테이션설명
@ExtendWith(MockitoExtension.class)Mockito를 사용하여 모킹하기 위해 테스트 클래스에 적용
@WebMvcTest웹 MVC 테스트를 위해 스프링 컨텍스트를 구성
@AutoConfigureJsonTestersJSON 테스트를 위해 JsonTester의 자동 구성을 활성화
@AutoConfigureMockMvcMockMvc를 자동으로 구성하는 데 사용
@Mock모킹 대상 객체를 생성하여 주입
@MockBeanSpring 컨텍스트에서 Mock 객체를 생성하여 주입
@SpringBootTest스프링 부트 애플리케이션의 통합 테스트를 위해 스프링 컨텍스트를 구성
@BeforEach각각의 테스트 메서드가 실행되기 전에 실행되는 메서드를 지정
@Test테스트 메서드를 지정

5. 초기환경 구성

5.1 의존성 주입

  • MockMVC를 사용하기 위해서는 ‘spring-boot-starter-test’ 라이브러리를 추가해야 한다.
  • 또한 Mockito 라이브러리 기능도 함께 사용하기에 의존성을 추가한다.
dependencies {
    testImplementation 'org.springframework.boot:spring-boot-starter-test'  // Spring Boot Starter Test
    testImplementation 'org.mockito:mockito-core:5.8.0'                     // Mockito Core
}

5.2 MockMvc 단계 별 구성 과정 : Controller 테스트

MockMvc에서도 역시 Mock 객체를 초기화하는 데 사용된다. 이를 수행하면 테스트에서 Mock 객체를 사용할 수 있고 테스트를 실행할 때 예상된 동작을 가진 Mock 객체를 사용할 수 있다.

어노테이션사용 버전설명
@ExtendWith(MockitoExtension.class)JUnit 5- Mockito를 사용하여 테스트 클래스를 초기화하는 데 사용
- MockitoExtension은 JUnit 5의 확장 기능으로,
Mockito의 기능을 테스트 클래스에 적용

MockMockito와 함께 사용되므로 초기화를 수행한다.

@ExtendWith(MockitoExtension.class)
class CodeControllerTest {	
}

5.3 Mock Annotation & MockMvc 구성

  • MockMvcController를 테스트하는 데 사용되기에 Controller@InjectMocks으로 객체를 생성하고 인스턴스를 주입한다.
    1. @InjectMocks Controller : @InjectMocks으로 객체를 생성하고 인스턴스를 주입한다.
    2. private MockMvc mockMvc : MockMvc를 선언한다.
    3. @BeforeEach : 선언한 MockMvcMockMvcBuilders.standaloneSetup(Class)를 통해 @Test가 실행되기 전에 독립적인 클래스(Controller)를 위해 선언하고 인스턴스를 생성하기 위해 구성한다.
어노테이션설명
@Mock모의 객체(Mock Object)를 생성하는데 사용
@InjectMocks모의 객체를 생성하고 인스턴스를 주입하는데 사용
@ExtendWith(MockitoExtension.class)
class CodeControllerTest {
    @InjectMocks
    private CodeController codeController;
    private MockMvc mockMvc;
}

5.4 MockMvcBuilders.standaloneSetup(Class) 구성

선언된 MockMvc에 MockMvcBuilders.standaloneSetup(Class)에 의해 @Test가 실행되기 전, 독립적인 클래스(Controller)를 위해 선언하며 또한 인스턴스를 생성하기 위해 구성합니다.

@ExtendWith(MockitoExtension.class)
class CodeControllerTest {
    @InjectMocks
    private CodeController codeController;

    private MockMvc mockMvc;

    @BeforeEach
    void setUp() {
        mockMvc = MockMvcBuilders
            .standaloneSetup(codeController)
            .build();
    }
}

6. MockMvc 테스트 방식

image-20240122210855816
테스트 방식설명
Standalone 테스트- 독립적으로 테스트를 수행한다는 것을 의미
- 다른 의존성 없이 테스트를 실행할 수 있음
- 외부 리소스나 서버에 대한 연결 없이 테스트를 수행이 가능
Spring Context를 수행하여 테스트- Spring Context를 실행하여 테스트를 수행
- 의존성 주입과 같은 Spring의 기능 사용이 가능
Web Server를 수행하여 테스트- Web Server를 실행하여 테스트를 수행
- 실제 서버에 대한 테스트를 가능
- 외부 리소스와의 상호작용을 테스트가 가능

6.1 MockMvc 활용 예제 -1: 웹 서버 없이 테스트

  • 해당 테스트에서는 ‘웹 서버’와 Spring Context 수행 없이 컨트롤러 로직을 테스트하는 경우이다.
  • Spring에서는 독립형 모드에서 MockMVC를 사용하면 내부 서버 테스트를 작성할 수 있으므로 Spring 컨텍스트를 로드하지 않는다.
Test MockMVC Standalone

6.2 Controller 테스트 구조 및 사용 예시

  • 주로 엔드포인트 요청에 따른 응답 값을 반환하고 있는지를 테스트를 수행한다.
  • 그렇기에 특정 API 엔드포인트에 요청 값을 담아서 보내면 응답값으로 전달이 되는지에 대해 확인한다.
package com.adjh.multiflexapi.controller;

import com.adjh.multiflexapi.common.codes.SuccessCode;
import com.adjh.multiflexapi.common.response.ApiResponse;
import com.adjh.multiflexapi.model.CodeDto;
import com.adjh.multiflexapi.service.CodeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

/**
 * 코드 정보를 관리하는 Controller
 */
@Slf4j
@RestController
@RequestMapping(value = "/api/v1/code")
@Tag(name = "Code", description = "코드 API")
public class CodeController {

    private final CodeService codeService;

    public CodeController(CodeService codeService) {
        this.codeService = codeService;
    }

    /**
     * [API] 코드 리스트 출력
     *
     * @param codeDto codeDto
     * @return ApiResponseWrapper<ApiResponse> : 응답 결과 및 응답 코드 반환
     */
    @PostMapping("/codes")
    @Operation(summary = "코드 조회", description = "코드 조회")
    public ResponseEntity<ApiResponse<Object>> selectCodeList(@RequestBody @Validated CodeDto codeDto) {
        log.debug("코드의 모든 리스트를 조회합니다.");
        ApiResponse<Object> ar = ApiResponse.builder()
                .result(codeDto)
                .resultCode(SuccessCode.SELECT.getStatus())
                .resultMsg(SuccessCode.SELECT.getMessage())
                .build();
        return new ResponseEntity<>(ar, HttpStatus.OK);
    }

    /**
     * [API] 코드 단건 조회 : 코드 키 값을 기반으로 조회합니다.
     *
     * @param cd String
     * @return ApiResponseWrapper<ApiResponse> : 응답 결과 및 응답 코드 반환
     */
    @GetMapping("/code")
    @Operation(summary = "코드값 별 코드 조회", description = "코드 조회")
    public ResponseEntity<ApiResponse<Object>> selectCodeByCd(@RequestParam String cd) {
        CodeDto codeItem = codeService.selectCodeByCd(cd);
        ApiResponse<Object> ar = ApiResponse.builder()
                .result(codeItem)
                .resultCode(SuccessCode.SELECT.getStatus())
                .resultMsg(SuccessCode.SELECT.getMessage())
                .build();
        return new ResponseEntity<>(ar, HttpStatus.OK);
    }

위에 Restful api를 기반으로 CodeControllerTest.java 파일을 자동생성하여 아래와 같이 구성하였습니다.

  • 자동생성 방법은 위에 환경 설정 부분을 참고하시면 됩니다.
img

6.3 Controller : 조회 테스트(요청 파라미터 전달)

Controller : 조회 테스트selectCodeList() 객체를 받아서 전체를 조회하는 메서드와 selectCodeByCd() String 문자열 키를 받아서 조회하는 메서드에 대해 테스트를 진행합니다.

사용예시

  • "/api/v1/code/code" 엔드포인트에 대한 GET 요청을 수행하고, "java"라는 코드 파라미터를 전달하여 테스트하고 있습니다.
  • 테스트 결과로는 MockMvcandExpect 메서드를 사용하여 상태코드가 200인지 확인하고, andDo 메서드를 사용하여 응답 내용을 출력하고 있습니다.
@Slf4j
@ExtendWith(MockitoExtension.class)
class CodeControllerTest {

    @InjectMocks
    private CodeController codeController;

    @Mock
    private CodeService codeService;

    private MockMvc mockMvc;

    @BeforeEach
    void setUp() {
        mockMvc = MockMvcBuilders.standaloneSetup(codeController).build();
    }

    @Test
    @DisplayName("코드 단건을 조회합니다.")
    void selectCodeByCd() throws Exception {
        // given
        String paramCd = "java";

        // when
        ResultActions resultActions = mockMvc.perform(
                MockMvcRequestBuilders.get("/api/v1/code/code")
                        .param("cd", paramCd)
                        .contentType(MediaType.APPLICATION_JSON));

        // then
        MvcResult mvcResult = resultActions
                .andExpect(status().isOk())
                .andDo(print())
                .andReturn();

        System.out.println("mvcResult :: " + mvcResult.getResponse().getContentAsString());
    }
}
image-20240122212755277

[참고] 필수 파라미터를 전달하지 않은 경우

image-20240122212843891

[참고] @Mock CodeService codeService;를 선언하고 사용하지 않는데 아래와 같은 오류가 발생하는 이유는?

  • codeServicenull인 상태에서 CodeControllerselectCodeByCd 메서드를 호출할 때 NullPointerException이 발생하기 때문입니다. codeServiceCodeController의 의존성으로 주입되어야 하지만, codeService가 없는 상태로 테스트를 진행하면 codeServicenull로 초기화되어 있어서 해당 메서드를 호출할 수 없기 때문에 오류가 발생합니다.
image-20240122213034074

6.4 Controller: 조회 테스트(객체 전달)

사용예시

  • "/api/v1/code/codes" 엔드포인트에 대한 POST 요청을 수행하고, 객체를 전달하여 테스트하고 있습니다.
  • 테스트 결과로는 MockMvcandExpect 메서드를 사용하여 상태코드가 200인지 확인하고, andDo 메서드를 사용하여 응답 내용을 출력하고 있습니다.
@Slf4j
@ExtendWith(MockitoExtension.class)
class CodeControllerTest {

    @InjectMocks
    private CodeController codeController;

    @Mock
    private CodeService codeService;

    private MockMvc mockMvc;

    @BeforeEach
    void setUp() {
        mockMvc = MockMvcBuilders.standaloneSetup(codeController).build();
    }

    @Test
    @DisplayName("코드 전체 리스트를 조회합니다.")
    void selectCodeListTest() throws Exception {

        // given
        CodeDto codeDto = CodeDto.builder().cd("java").build();

        // when
        ResultActions resultActions = mockMvc.perform(
                MockMvcRequestBuilders.post("/api/v1/code/codes")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(new Gson().toJson(codeDto)));
        // then
        MvcResult mvcResult = resultActions
                .andExpect(status().isOk())
                .andDo(print())
                .andReturn();

        System.out.println("mvcResult :: " + mvcResult.getResponse().getContentAsString());
    }
}

image-20240122213235402

7. MockMvc 활용 예제 -2: Spring Context를 사용하여 테스트

테스트 활용 구조

- 해당 테스트 이전 구조에서는 다르게 Web Server를 직접적으로 사용하지 않지만 ‘Spring Context’까지 사용하여 테스트를 진행하는 방법입니다.
- 해당 방식은 @WebMVCTest 어노테이션을 통해 MockMvc 인스턴스가 자동으로 구성되고 컨텍스트에서 사용이 가능해집니다.

img

[ 더 알아보기 ]

💡 웹 서버(Web Server)

  • 클라이언트의 요청을 받아들이고, 처리한 결과를 클라이언트에게 전달하는 역할을 수행하는 소프트웨어입니다.
  • 웹 서버는 HTTP 프로토콜을 사용하여 클라이언트와 통신하며, 주로 웹 애플리케이션을 호스팅 하고 실행하는 역할을 합니다.

💡 스프링 컨텍스트(Spring Context)

  • 스프링 프레임워크에서 제공하는 기능 중 하나로, 애플리케이션의 구성 요소들을 관리하고 제어하는 역할을 합니다.
  • 스프링 컨텍스트는 애플리케이션의 객체들을 생성하고 관리하며, 의존성 주입(Dependency Injection)을 통해 객체 간의 관계를 설정합니다.

💡 해당 테스트 구조를 본다면 웹 서버와 스프링 컨텍스트의 독립적인 관계로 이해하면 되나?

  • 각각의 기능으로 봤을 때 스프링 컨텍스트는 웹 애플리케이션의 객체 관리와 제어에 사용되는 독립적인 컨테이너이며, 웹 서버는 클라이언트의 요청을 받아들이고 처리한 결과를 전달하는 역할을 수행합니다.
  • 따라서, 스프링 컨텍스트와 웹 서버는 서로 다른 역할을 가지고 있으며, 독립적으로 실행될 수 있습니다.

7.1 Controller 테스트 구조 및 사용 예시

사용 예시

  • 해당 테스트에서는 @WebMvcTest(CodeController.class)를 통해서 슬라이스 테스트를 진행하였습니다.
  • 이를 통해 Spring Context를 실행하여 테스트를 하며 이전에 standalone 방식을 사용하지 않고 수행하였습니다.
package com.adjh.multiflexapi.controller;

import com.adjh.multiflexapi.model.CodeDto;
import com.adjh.multiflexapi.service.CodeService;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.AutoConfigureJsonTesters;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
 * MockMvc 활용하여 Spring Context를 실행시켜 테스트
 *
 * @author : lee
 * @fileName : CodeControllerSpringContext
 * @since : 12/14/23
 */
@Slf4j
@AutoConfigureJsonTesters
@WebMvcTest(CodeController.class)
class CodeControllerSpringContext {

    @MockBean
    private CodeController codeController;

    @Mock
    private CodeService codeService;

    @Autowired
    private MockMvc mockMvc;

    @BeforeEach
    void setUp() {
    }

    @Test
    @DisplayName("코드 단건을 조회합니다.")
    void selectCodeByCd() throws Exception {

        // given
        String paramCd = "java";

        // when
        ResultActions resultActions = mockMvc.perform(
                MockMvcRequestBuilders.get("/api/v1/code/code")
                        .param("cd", paramCd)
                        .contentType(MediaType.APPLICATION_JSON));

        // then
        MvcResult mvcResult = resultActions
                .andExpect(status().isOk())
                .andDo(print())
                .andReturn();

        System.out.println("mvcResult :: " + mvcResult.getResponse().getContentAsString());
    }

    @Test
    @DisplayName("코드 전체 리스트를 조회합니다.")
    void selectCodeListTest() throws Exception {

        // given
        CodeDto codeDto = CodeDto.builder().cd("java").build();

        // when
        ResultActions resultActions = mockMvc.perform(
                MockMvcRequestBuilders.post("/api/v1/code/codes")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(new Gson().toJson(codeDto)));
        // then
        MvcResult mvcResult = resultActions
                .andExpect(status().isOk())
                .andDo(print())
                .andReturn();

        System.out.println("mvcResult :: " + mvcResult.getResponse().getContentAsString());
    }
}

💡 아래와 같이 컨텍스트가 수행되었지만 ‘401 Unauthorized’ 에러가 발생한 것을 확인할 수 있습니다.

  • 해당 문제가 발생한 이유는 해당 코드에서 @WebMvcTest(CodeController.class) 어노테이션이 사용되어 특정 컨트롤러만을 대상으로 하는 슬라이스 테스트를 수행하고 있기 때문입니다.
  • 이러한 슬라이스 테스트는 보안 및 인증과 관련된 기능을 포함하지 않습니다. 따라서, 인증되지 않은 요청이 발생하게 되면 401 Unauthorized 오류가 반환됩니다.
img img

8. MockMvc 활용 예제 -3 : 웹 서버를 사용하여 테스트

테스트 활용 구조

  • 해당 구조에서는 이전 구조와 다르게 Spring Context까지 사용했던 부분에서 이어나가 웹 서버까지 사용하여 테스트를 진행하는 방법입니다.
  • @SpringBootTest 어노테이션을 통해 실제 HTTP 서버로 테스트를 진행하는 방법입니다.
  • https://thepracticaldeveloper.com/guide-spring-boot-controller-tests/
@SpringBoot test approach

8.1 Controller 테스트 구조 및 사용 예시

사용예시

  • 해당 테스트에서는 @SpringBootTest 어노테이션을 통해서 Web Server를 수행하도록 테스트를 진행하였습니다.
  • TestRestTemplate을 통해서 HTTP 요청을 보내고 응답받는 형태로 체크를 하는 형태로 수행하였습니다.
package com.adjh.multiflexapi.controller;

import com.adjh.multiflexapi.model.CodeDto;
import com.adjh.multiflexapi.service.CodeService;
import groovy.util.logging.Slf4j;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;
import org.springframework.test.web.servlet.MockMvc;

/**
 * Web Server 수행시켜 테스트를 하는 경우
 *
 * @author : lee
 * @fileName : CodeControllerWebServerTest
 * @since : 12/14/23
 */
@Slf4j
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class CodeControllerWebServerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private CodeService codeService;

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    @DisplayName("코드 단건을 조회합니다.")
    void selectCodeByCd() throws Exception {

        // given
        String paramCd = "java";

        // when
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", "Bearer xxxxxxx");

        // then
        HttpEntity<String> entity = new HttpEntity<>(headers);
        String url = String.format("/api/v1/code/code?cd=%s", paramCd);
        ResponseEntity<CodeDto> codeDtoResult = restTemplate.exchange(url, HttpMethod.GET, entity, CodeDto.class);
        Assertions.assertEquals(codeDtoResult.getStatusCode(), HttpStatus.OK);
        System.out.println("codeDtoResult ::" + codeDtoResult);
    }

    @Test
    @DisplayName("코드 전체 리스트를 조회합니다.")
    void selectCodeListTest() throws Exception {

        // given
        CodeDto codeDto = CodeDto.builder().cd("java").build();

        // when
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", "Bearer xxxxxx");

        // then
        HttpEntity<CodeDto> entity = new HttpEntity<>(codeDto, headers);
        ResponseEntity<CodeDto> codeDtoResult = restTemplate.exchange("/api/v1/code/codes", HttpMethod.POST, entity, CodeDto.class);
        Assertions.assertEquals(codeDtoResult.getStatusCode(), HttpStatus.OK);
        System.out.println("codeDtoResult ::" + codeDtoResult);
    }
}

사용 결과

  • 아래와 같이 서버가 수행되고 TestRestTemplate을 통해서 HTTP 통신을 통해 호출이 되어 정상임을 테스트하였습니다.
img
profile
필기하고, 타이핑하고, 말하면서 읽고, 코딩하고, 눈으로 읽고 오감으로 공부하기

0개의 댓글