스프링부트에서는 배포나 직접 localhost로 접근하여 여러 가지 경우를 테스트해 보는 것 말고 더 편하게 내가 쓴 코드를 테스트할 수 있는 기능을 제공한다. 그래서 테스트 코드를 어떻게 작성해야 하는지를 기록해보려 한다. 주로 JUnit을 활용한 테스트 방법을 기술한다. 아래 책을 공부 중이다.
신선영, 『스프링 부트3 백엔드 개발자 되기』, 골든레빗(2024)
1. 기본적인 test 방식
우선 테스트 코드를 작성할 때는 크게 아래의 흐름을 따른다.
given (이런 상황이 주어졌는데)
when (이렇게 됐을 때)
then (결과가 이렇게 되어야 해)
예를 들어, 기본적인 회원가입 로직을 테스트하기 위해서는 given으로 회원가입하려는 멤버 정보가 주어져야 한다. 그리고 when에서는 회원가입하는 상황, 즉 db에 회원을 추가하는 상황이 발생할 것이고, then에서는 db에 저장된 회원 정보가 given에서 주어진 멤버 정보가 일치하는지 확인해야 한다.
@Test
void 회원가입() {
// given (이런 상황이 주어졌는데)
Member member = new Member();
member.setName("hello");
// when (이렇게 됐을 때)
Long saveId = memberService.join(member);
// then (결과가 이렇게 되어야 해)
Member findMember = memberService.findOne(saveId).get();
Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());
}
코드를 작성하는 관점으로 보면, given은 테스트를 준비하는 단계이고, when은 테스트를 실제로 진행하는 단계, 마지막으로 then은 테스트 결과를 검증하는 단계로 볼 수 있다.
2. test 관련 annotation
자세한 테스트 코드를 보기 전, 테스트에 관련 annotation 몇 가지를 소개하려 한다. 먼저 테스트를 하려면 꼭 필요한 기본 annotation이다.
- @SpringBootTest: 통합 테스트를 위한 class에 넣는 annotation
- @Test: 단위 테스트를 위한 method에 넣는 annotation
다음은 알아두면 유용한 annotation이다.
- @Transactional: 테스트 케이스 종료 후 이전의 DB 상태로 돌아가도록 롤백함 (AfterEach를 통해 지워주지 않아도 DB에 데이터가 남지 않아 반복적인 테스트가 가능)
- @BeforeEach, @AfterEach: 각 테스트 시작 전 혹은 종료 후에 반복되는 코드
- @BeforeAll, @AfterAll: 모든 테스트 시작 전 혹은 종료 후에 실행되는 코드 (주로 초기 설정이나 종료 후 메모리 정리 등으로 사용)
- @AutoconfigureMockMvc: MockMvc를 생성하고 자동으로 구성하는 annotation. 테스트용 mvc 환경을 만들어 요청 및 전송, 응답 기능을 제공
- @DisplayName: 테스트 이름을 명시. 뭘 테스트하는지, 결과가 어떻게 되어야 하는지를 메모해 두면 좋음.
3. test 코드 예시
package com.study.springbootpractice;
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.result.MockMvcResultMatchers.content;
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.status;
import com.study.springbootpractice.QuizController.Code;
@SpringBootTest
@AutoConfigureMockMvc
class QuizControllerTest {
@Autowired
protected MockMvc mockMvc;
@Autowired
private WebApplicationContext context;
@Autowired
private ObjectMapper objectMapper;
@BeforeEach
public void mockMvcSetUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build();
}
@DisplayName("quiz(): GET /quiz?code=1 이면 응답 코드는 201, 응답 본문은 Created!를 리턴한다.")
@Test
public void getQuiz1() throws Exception {
// given
String getUrl = "/quiz";
// when
final ResultActions result = mockMvc.perform(get(getUrl).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 {
String getUrl = "/quiz";
final ResultActions result = mockMvc.perform(get(getUrl).param("code", "2"));
result
.andExpect(status().isBadRequest())
.andExpect(content().string("Bad Request!"));
}
@DisplayName("quiz(): POST /quiz에 요청 본문이 {\"value\":13}이면 응답코드는 200, 응답 본문은 OK!를 리턴한다")
@Test
public void postQuiz13() throws Exception {
String postUrl = "/quiz";
final ResultActions result = mockMvc.perform(post(postUrl)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new Code(13))));
result
.andExpect(status().isOk())
.andExpect(content().string("OK!"));
}
@DisplayName("quiz(): POST /quiz에 요청 본문이 {\"value\":1}이면 응답 코드는 403, 응답 본문은 Forbidden!을 리턴한다.")
@Test
public void postQuiz1() throws Exception {
String postUrl = "/quiz";
final ResultActions result = mockMvc.perform(post(postUrl)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new Code(1))));
result
.andExpect(status().isForbidden())
.andExpect(content().string("Forbidden!"));
}
}
코드를 작성하면서 새로 알게된 부분을 정리하면 아래와 같다.
andExpect()
andExpect로 기대하는 값을 넣으면, 해당 값과 일치하는지를 확인하여 test 결과가 나온다. 위 코드에서 볼 수 있듯, status()나 content() 등을 확인할 수 있다.
mockMVC 실행 시 param으로 정보 전달 시와 요청 본문을 전달 시의 차이
위 코드를 보면, get 요청을 할 때는 url 내 파라미터로 code를 넘기기 때문에 get 요청에 param을 추가하여 요청을 전송했다. 하지만 post 요청을 보낼 때는 json형태로 요청 본문을 보내야 하기 때문에 ObjectMapper를 활용하여 code 정보를 주어진 content type으로 변환하여 전달했다. 이렇게 어떤 형식으로 전달하는지에 따라서 방법의 차이가 있다.
'웹 백엔드 개발 > Spring Boot' 카테고리의 다른 글
[API] 알아두면 유용한 응답코드 (0) | 2025.03.08 |
---|---|
[Spring Booot] 백엔드 기본 개념 (DTO, Entity, DAO) (0) | 2025.01.11 |
[Spring Boot] 스프링부트 기본 개념 (MVC, IoC, DI, AOP, PSA) (0) | 2025.01.07 |
[Spring Boot / Kotlin] 튜토리얼4, Spring Data CrudRepository 활용하기 (0) | 2025.01.07 |
[Spring Boot / Kotlin] 튜토리얼3, data base 추가하기 (0) | 2025.01.06 |