FrameWork/Spring Boot

MockMvc를 사용하여 컨트롤러를 테스트하는 코드 작성해보기

soooy0 2025. 3. 4. 22:34

 

이제 제대로 테스트 코드를 작성해보자.

 

1. TestController클래스의 이름에 클릭을 한 다음, ctrl + enter를 눌러서 [Create Test]를 누른다.

 

2. 테스트를 생성하면, 자동으로 test/java/패키지 아래에 테스트 파일이 생성된다.

 

 

3. 아래 코드 작성

package me.jinsoyeong.springbootdeveloper;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
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.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;


import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.junit.jupiter.api.Assertions.*;

/*
* @SpringBootTest 애너테이션은 메인 애플리케이션 클래스에 추가하는 애너테이션(@SpringBootApplication)이 있는 클래스를 찾는다.
* 그리고 그 클래스에 포함되어 있는 빈을 찾은 다음, 테스트용 애플리케이션 컨텍스트라는 것을 만든다.
* */
@SpringBootTest // 테스트용 어플리케이션 컨텍스트 생성

/*
* @AutoConfigureMockMvc 애너테이션은 MockMvc를 생성하고 자동으로 구성하는 애너테이션이다.
* MockMvc는 애플리케이션을 서버에 배포하지 않고도 테스트용 MVC환경을 만들어 요청 및 전송, 응답 기능을 제공하는 유틸리티 클래스이다.
* 즉, 컨트롤러를 테스트 할 때 사용되는 클래스이다.
* */
@AutoConfigureMockMvc // MockMvc 생성 및 자동 구성
class TestControllerTest {

    @Autowired
    protected MockMvc mockMvc;

    @Autowired
    private WebApplicationContext context;

    @Autowired
    private MemberRepository memberRepository;

    // 테스트 전 실행 메서드
    @BeforeEach
    public void mockMvcSetUp() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
                .build();
    }

    // 테스트 후 실행 메서드
    @AfterEach
    public void cleanUp(){
        memberRepository.deleteAll();
    }

    /*
    * TestController의 로직을 테스트 하는 코드 작성
    * */
    @DisplayName("getAllMembers: 아티클 조회에 성공한다.")
    @Test
    public void getAllMembers() throws Exception {
        // given(멤버를 저장한다)
        final String url = "/test";
        Member savedMember = memberRepository.save(new Member(1L, "홍길동"));

        // when
        /*
        * perform() 메서드
        *   요청을 전송하는 역할을 하는 메서드이다. 결과로 ResultActions 객체를 받으며,
        *   ResultActions 객체는 반환값을 검증하고 확인하는 andExcept()메서드를 제공한다.
        *
        * accept() 메서드
        *   요청을 보낼 때 무슨 타입으로 응답을 받을 지 결정하는 메서드. JSON, XML등 다양한 타입이 있지만,
        *   여기에서는 JSON을 받는다고 명시해두도록 한다.
        * */
        final ResultActions result = mockMvc.perform(get(url)
                .accept(MediaType.APPLICATION_JSON));

        /*
        * andExpect()메서드
        *   응답을 검증한다. TestController에서 만든 API는 응답으로 OK(200)을 반환하므로 이에 해당하는 메서드인 isOK를 사용하여
        *   응답코드가 OK(200)인지 확인한다.
        *
        * jsonPath("$[0].${필드명}")
        *   JSON 응답값의 값을 가져오는 역할을 한느 메서드
        *   0번째 배열에 들어있는 객체의 id, name값을 가져오고, 저장된 값과 같은지 확인한다.
        * */
        // then
        result
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].id").value(savedMember.getId()))
                .andExpect(jsonPath("$[0].name").value(savedMember.getName()));
    }
}

 

  • 이때, 롬복 플러그인을 설치해야 오류가 없어진다. 롬복을 사용하기 위한 의존성만 추가한 상태이므로, 인텔리제이에서 lombok을 사용하려면 플로그인도 다운로드 받아야 한다. [Settings -> Plugins->Marketplace]에서 롬복을 검색한 뒤, 다운로드 하고 인텔리제이를 다시 시작한다.
  • 위 테스트 코드에서는 Given(멤버지정)-when(멤버리스트를 조회하는 API호출)-Then(응답코드가 200 OK이고, 반환받은 값 중에 0번째 요소의 id와 name이 저장된 값과 같은지 확인) 패턴이 적용되어 있다.

 

위 테스트 코드를 찬찬히 뜯어보자.

@SpringBootTest
@AutoConfigureMockMvc
class TestControllerTest {
    @Autowired
    protected MockMvc mockMvc;

    @Autowired
    private WebApplicationContext context;

    @Autowired
    private MemberRepository memberRepository;
    @BeforeEach
    public void mockMvcSetUp() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
                .build();
    }
    @AfterEach
    public void cleanUp() {
        memberRepository.deleteAll();
    }

 

@SpringBootTest
  • @SpringBootTest 애너테이션은 메인 애플리케이션 클래스에 추가하는 애너테이션(@SpringBootApplication)이 있는 클래스를 찾는다.
  • 그리고 그 클래스에 포함되어 있는 빈을 찾은 다음, 테스트용 애플리케이션 컨텍스트라는 것을 만든다.
  • Spring Boot 애플리케이션을 테스트 환경에서 실행하도록 설정. 모든 빈(bean)이 로드되므로 통합 테스트에 적합함.

@AutoConfigureMockMvc
  • MockMvc를 생성하고 자동으로 구성하는 애너테이션이다.
  • 애플리케이션을 서버에 배포하지 않고도 테스트용 MVC환경을 만들어 요청 및 전송, 응답 기능을 제공하는 유틸리티 클래스이다.
  • 즉, 컨트롤러를 테스트 할 때 사용되는 클래스이다.

 

테스트에 필요한 빈 주입
// 실제 HTTP 요청을 보내지 않고, 가상의 요청을 만들어 컨트롤러를 테스트할 수 있도록 해줌. 
@Autowired
protected MockMvc mockMvc;

// MockMvc를 Spring의 전체 컨텍스트와 함께 설정하기 위해 사용됨.
@Autowired
private WebApplicationContext context;

// 테스트할 데이터(Member)를 저장하고 조회하기 위해 JPA Repository를 주입받음.
@Autowired
private MemberRepository memberRepository;

 

[추가]
Q. 그럼 컨텍스트가 무엇을 의미하는 걸까?
A. Spring 컨테이너라고도 불리며, 스프링 애플리케이션의 모든 빈(Bean)과 설정 정보를 관리하는 객체이다.
쉽게 말해서, 스프링 애플리케이션의 "뇌" 같은 역할을 한다. 스프링이 관리하는 모든 빈(객체)들을 담고 있는 컨테이너!

🔹 그럼 WebApplicationContext란?

- WebApplicationContext는 웹 애플리케이션 전용 스프링 컨텍스트이다.
- 일반적인 ApplicationContext와 비슷하지만, 웹 관련 빈(예: 컨트롤러, 필터, 세션 등)을 추가로 관리한다.
- 즉, 웹 환경에서 동작하는 모든 빈을 담고 있는 컨테이너라고 보면 된다.

🔹 MockMvc를 컨텍스트(WebApplicationContext) 기반으로 설정하는 이유?

- mockMvc를 WebApplicationContext 기반으로 설정하면 실제 스프링 컨테이너에서 관리하는 빈(Controller, Service, Repository 등)을 그대로 사용할 수 있다.
- 즉, 실제 스프링 환경과 최대한 비슷하게 테스트 가능하게 해준다.

🔹 MockMvc를 컨텍스트(WebApplicationContext) 기반으로 설정하지 않는다면?

컨텍스트를 사용하지 않고 컨트롤러만 테스트하고 싶다면
this.mockMvc = MockMvcBuilders.standaloneSetup(new TestController()).build(); 이렇게 코드 작성하면 됨.
standaloneSetup()을 사용하면 컨트롤러만 테스트 가능 하지만, Service, Repository 같은 빈은 직접 Mock 객체로 만들어서 주입해야 한다. 즉, 컨트롤러만 단독 테스트 가능 (서비스, DB 연동은 안되니까 따로 주입해야 함)

 

테스트 실행 전 MockMvc 설정(@BeforeEach)
// 테스트 전 실행 메서드
    // MockMvc를 WebApplicationContext 기반으로 설정하여, 
    // 스프링의 전체 웹 애플리케이션 컨텍스트를 로드한 상태에서 테스트가 가능하도록 함.
    @BeforeEach
    public void mockMvcSetUp() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
                .build();
    }

 

테스트 실행 후 데이터 정리(@AfterEach)
    // 테스트 후 실행 메서드
    // Member 엔터티에 대한 데이터를 모두 삭제하여 다음 테스트에 영향을 주지 않도록 정리하는 역할.
    @AfterEach
    public void cleanUp(){
        memberRepository.deleteAll();
    }

 

 

 

실제 테스트 코드 작성 시작(@DisplayName, @Test 어노테이션~ 끝)
@DisplayName
  • 테스트의 목적을 명확히 표시하는 애너테이션
  • 테스트 실행 시 콘솔에서 확인하기 쉬움
getAllMembers() 메서드 작성
given, 테스트 준비
// given(멤버를 저장한다)
        // 호출할 API 엔드포인트 지정
        final String url = "/test";
        // Member객체를 DB에 저장하여 이후 API 요청에서 데이터를 조회할 수 있도록 준비
        Member savedMember = memberRepository.save(new Member(1L, "홍길동"));

 

when, API 요청 실행
/*
        * perform() 메서드
        *   요청을 전송하는 역할을 하는 메서드이다. 결과로 ResultActions 객체를 받으며,
        *   ResultActions 객체는 반환값을 검증하고 확인하는 andExcept()메서드를 제공한다.
        *   Get /test 요청을 보낸다.
        *   실제 API를 호출하는 것처럼 동작하지만, 내부적으로 가상의 요청을 만들어서 실행한다.
        *
        * accept() 메서드
        *   요청을 보낼 때 무슨 타입으로 응답을 받을 지 결정하는 메서드. JSON, XML등 다양한 타입이 있지만,
        *   여기에서는 JSON을 받는다고 명시해두는 것.
        * */
        final ResultActions result = mockMvc.perform(get(url)
                .accept(MediaType.APPLICATION_JSON));

 

then, 검증(Response 체크)
/*
    * andExpect() 메서드
    *   응답을 검증한다. TestController에서 만든 API는 응답으로 OK(200)을 반환하므로 이에 해당하는 메서드인 isOK를 사용하여
    *   응답코드가 OK(200)인지 확인한다.
    *
    * jsonPath("$[0].${필드명}")
    *   JSON 응답값의 값을 가져오는 역할을 하는 메서드
    *   0번째 배열에 들어있는 객체의 id, name값을 가져오고, saveMember의 id, name과 같은지 확인한다.
    * */
    // then
    result
            .andExpect(status().isOk())
            // 응답의 0번째 값이 DB에 저장한 값과 같은지 확인한다.
            .andExpect(jsonPath("$[0].id").value(savedMember.getId()))
            .andExpect(jsonPath("$[0].name").value(savedMember.getName()));
}

 

Q. 나는 Member 테이블을 DB에 생성한 적이 없는데, 어떻게 사용 가능한 걸까?

A. 이전, gradle 설정에서 H2 인메모리 사용 설정을 해두었다.
spring boot가 실행되면서 H2같은 인메모리 DB가 생성된다. -> test코드에서 save() 호출하면서 비어있던 테이블에 데이터가 저장되고 -> 테스트 실행 중에만 유지되며, 테스트가 끝나면 메모리에서 삭제되면서 데이터도 사라진다.

 

 

Quiz
src- main -.. - springbootdeveloper 폴더 안에 QuizController.java 클래스 생성
package me.jinsoyeong.springbootdeveloper;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
public class QuizController {


    /*
    * 메서드: Get /quiz 요청이 오면, quiz() 메서드로 요청을 처리하는 메서드
    *   'code'라는 요청 파라미터의 키를 받아서, int 타입 변수로 저장 : ex) /quiz?code=1 -> code = 1
    *   code 값에 따라 다른 응답을 보낸다.
    *
    * 반환: ResponseEntity(spring 에서 HTTP 응답을 조작할 수 있도록 도와주는 클래스) */
    @GetMapping("/quiz")
    public ResponseEntity<String> quiz(@RequestParam("code") int code) {
        switch (code) {
            // code = 1일 경우, 201 Created 응답 반환, created()는 리소스가 생성됨을 의미, body()는 HTTP 응답의 본문을 설정하는 역할
            case 1:
                return ResponseEntity.created(null).body("Created!");
            // code = 2일 경우, 400 Bad Request 응답 반환, badRequest()는 잘못된 요청을 의미
            case 2:
                return ResponseEntity.badRequest().body("Bad Request!");
            // 그 외 경우, 기본적으로 200 ok 응답 반환, ok()는 정상적인 응답을 의미
            default:
                return ResponseEntity.ok().body("OK!");
        }
    }


    /*
    * 메서드: Post /quiz 요청이 오면 요청값을 code라는 객체로 매핑한 후에 value값에 따라 다른 응답을 보냄
    * 클라이언트가 JSON 데이터를 requestBody에 담아서 보내면, 그 데이터를 code 객체(자바 객체)로 받아서 처리하는 방식이다.*/
    @PostMapping("/quiz")
    public ResponseEntity<String> quiz2(@RequestBody Code code) {


        switch (code.value()) {
            case 1:
                return ResponseEntity.status(403).body("Forbidden!");
            default:
                return ResponseEntity.ok().body("OK!");
        }
    }
}

/* record 클래스
    불변 데이터 클래스로, 클래스의 기본적인 기능을 자동 생성해준다. getter(), toString(), equals(), hashCode()를 자동으로 만들어 줌
    Code 객체를 쉽게 생성 가능하다. spring boot에서 requestBody로 JSON 데이터를 받아올 때 편리하다.
*/
record Code(int value) {}

 

  • cmd + shift + T 단축키로 테스트 클래스를 만든다.
package me.jinsoyeong.springbootdeveloper;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
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.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

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.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
class QuizControllerTest {


    @Autowired
    protected MockMvc mockMvc;


    @Autowired
    private WebApplicationContext context;


    @Autowired
    private ObjectMapper objectMapper;


    @BeforeEach
    public void mockMvcSetUp() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
                .build();
    }

    @DisplayName("quiz(): GET /quiz?code=1 이면 응답 코드는 201, 응답 본문은 Created!를 리턴한다.")
    @Test
    public void getQuiz1() throws Exception {
        // given
        final String url = "/quiz";


        // when
        final ResultActions result = mockMvc.perform(get(url)
                .param("code", "1")
        );


        // then
        result
                .andExpect(status().isCreated())
                .andExpect(content().string("Created!"));
    }


    @DisplayName("quiz(): GET /quiz?code=2 이면 응답 코드는 400, 응답 본문은 Bad Request!를 리턴한다.")
    @Test
    public void getQuiz2() throws Exception {
        // given
        final String url = "/quiz";


        // when
        final ResultActions result = mockMvc.perform(get(url)
                .param("code", "2")
        );


        // then
        result
                .andExpect(status().isBadRequest())
                .andExpect(content().string("Bad Request!"));
    }


    @DisplayName("quiz(): POST /quiz?code=1 이면 응답 코드는 403, 응답 본문은 Forbidden!를 리턴한다.")
    @Test
    public void postQuiz1() throws Exception {
        // given
        final String url = "/quiz";


        // when
        final ResultActions result = mockMvc.perform(post(url)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(new Code(1)))
        );


        // then
        result
                .andExpect(status().isForbidden())
                .andExpect(content().string("Forbidden!"));
    }


    @DisplayName("quiz(): POST /quiz?code=13 이면 응답 코드는 200, 응답 본문은 OK!를 리턴한다.")
    @Test
    public void postQuiz13() throws Exception {
        // given
        final String url = "/quiz";


        // when
        final ResultActions result = mockMvc.perform(post(url)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(new Code(13)))
        );


        // then
        result
                .andExpect(status().isOk())
                .andExpect(content().string("OK!"));
    }
}

 

 

Test에 사용되는 메서드들이 생소해서 조금 어려운 것 같다. 일단 먼저 코드 파악하고, 다시 복습하면서 메서드 사용법에 익숙해져야겠다.