스프링 부트와 테스트

ppp·2025년 7월 10일

스프링 부트 구조

  • 스프링 부트는 각 계층이 양 옆의 계층과 통신하는 구조를 따른다. 계층은 각자의 역할과 책임이 있는 어떤 소프트웨어의 구성 요소를 의미한다. 각 계층은 소통할 수 있지만 다른 계층에 직접 간섭하거나 영향을 미치지 않는다.

  • 스프링 부트에는 프레젠테이션(컨트롤러), 비즈니스(서비스), 퍼시스턴스(리포지토리) 계층이 있다.

계층 구분역할 설명
프레젠테이션 계층- 클라이언트의 HTTP 요청을 수신
- 요청을 비즈니스 계층에 전달하고 결과를 응답
비즈니스 계층- 비즈니스 로직 처리 전담
- 서비스 목적에 맞는 핵심 기능 수행
퍼시스턴스 계층- 데이터베이스와의 연동 담당
- DAO(Data Access Object)를 통해 데이터 조작 수행

스프링 부트 요청-응답 과정

  1. 톰캣에 HTTP 요청을 한다. 이 요청은 스프링 부트 내로 이동한다. 이때 스프링 부트의 디스패처 서블릿이 URL을 분석하고, 이 요청을 처리할 수 있는 컨트롤러를 찾는다.

  2. 컨트롤러가 비즈니스 계층과 퍼시스턴스 계층을 통하면서 필요한 데이터를 가져온다.

  3. 뷰 리졸버는 템플릿 엔진을 사용해 HTML 문서를 만들거나 JSON, XML 등의 데이터를 생성한다.

  4. 디스패처 서블릿에 의해 응답으로 클라이언트에게 반환된다.

스프링 부트와 테스트

  • 스프링 부트는 애플리케이션을 테스트하기 위한 도구와 애너테이션을 제공한다.

  • spring-boot-starter-test 스타터에 테스트를 위한 도구가 모여 있다. 대표적으로 :

항목설명
JUnit자바 프로그래밍 언어용 단위 테스트 프레임워크
Spring Test & Spring Boot Test스프링 및 스프링 부트 애플리케이션을 위한 통합 테스트 지원 도구
AssertJ가독성 높고 풍부한 기능을 제공하는 어설션(검증문) 작성 라이브러리

JUnit

  • 자바 언어를 위한 단위 테스트 프레임워크

  • 단위 테스트란, 작성한 코드가 의도대로 작동하는지 작은 단위로 검증하는 것을 의미합니다. 이때 단위는 보통 메서드가 된다.

  • JUnit은 테스트 클래스에 정의된 각 테스트 메서드(@Test)를 실행할 때마다 새로운 인스턴스(객체)를 생성해서 테스트를 수행하고, 테스트가 끝나면 그 인스턴스를 즉시 폐기합니다.

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class JUnitTest {
  @DisplayName("1 + 2는 3이다")
  @Test
  public void junitTest() {
    int a = 1;
    int b = 2;

    int sum = 3;

    Assertions.assertEquals(sum, a + b);
  }
}
  • @DisplayName 애너테이션은 테스트 이름을 명시해준다.

  • @Test 애너테이션을 붙인 메서드는 테스트를 수행하는 메서드가 된다.

  • assertEquals() 메서드의 첫 번째 인수에는 기대하는 값, 두 번째 인수에는 실제로 검증할 값을 넣어준다.

Cycle

import org.junit.jupiter.api.*;

public class JUnitCycleTest {
  @BeforeAll
  static void beforeAll() {
    System.out.println("@BeforeAll");
  }

  @BeforeEach
  public void beforeEach() {
    System.out.println("@BeforeEach");
  }

  @Test
  public void test1() {
    System.out.println("test1");
  }

  @Test
  public void test2() {
    System.out.println("test2");
  }

  @Test
  public void test3() {
    System.out.println("test3");
  }

  @AfterAll
  static void afterAll() {
    System.out.println("@AfterAll");
  }

  @AfterEach
  public void afterEach() {
    System.out.println("@AfterEach");
  }
}

BeforeAll

  • 전체 테스트를 시작하기 전에 처음으로 한 번만 실행

  • 전체 테스트 주기에서 한 번만 호출되어야 하기 때문에 static으로 선언

BeforeEach

  • 테스트 케이스를 시작하기 전에 매번 실행

  • 각 인스턴스에 대해 메서드를 호출해야 하므로 메서드는 static이 아니어야 함

AfterAll

  • 전체 테스트를 마치고 종료하기 전에 한 번만 실행

  • 전체 테스트 주기에서 한 번만 호출되어야 하기 때문에 static으로 선언

AfterEach

  • 각 테스트 케이스를 종료하기 전 매번 실행

  • 각 인스턴스에 대해 메서드를 호출해야 하므로 메서드는 static이 아니어야 함

AssertJ

  • AssertJ는 JUnit과 함께 사용해 검증문의 가독성을 확 높여주는 라이브러리
import static org.assertj.core.api.Assertions.assertThat;

assertThat(a + b).isEqualTo(sum);
  • assertThat을 사용시 주로 static import 사용

  • 다양한 검증 메서드 제공 (isEqualTo, isNotEqualTo, contains, doesNotContain, startsWith, endsWith, isEmpty, isNotEmpty, isPositive, isNegative, isGreaterThan, isLessThan 등)

컨트롤러 테스트 코드 작성

@SpringBootTest

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

@AutoConfigureMockMvc

  • MockMvc를 생성하고 자동으로 구성하는 애너테이션

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

예제1

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

  @Autowired
  private WebApplicationContext context;

  @Autowired
  private MemberRepository memberRepository;

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

  @AfterEach
  public void cleanUp() {
    memberRepository.deleteAll();
  }

  @DisplayName("getAllMembers: 아티클 조회에 성공한다.")
  @Test
  public void getAllMembers() throws Exception {
    // given
    final String url = "/test";
    Member savedMember = memberRepository.save(new Member(1L, "홍길동"));

    // when
    final ResultActions result = mockMvc.perform(get(url)
        .accept(MediaType.APPLICATION_JSON));

    // then
    result
        .andExpect(status().isOk())
        .andExpect(jsonPath("$[0].id").value(savedMember.getId()))
        .andExpect(jsonPath("$[0].name").value(savedMember.getName()));
  }
}
  • perform() 메서드는 요청을 전송하는 역할을 하는 메서드이다. 그 결과로 ResultActions 객체를 받으며, ResultActions 객체는 반환값을 검증하고 확인하는 andExpect() 메서드를 제공해준다.

  • accept() 메서드는 요청을 보낼 때 무슨 타입으로 응답을 받을지 결정하는 메서드이다.

  • jsonPath() 메서드는 JSON 응답값의 값을 가져오는 역할을 하는 메서드이다.

예제2

@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 {
    String url = "/quiz";

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

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

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

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

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

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

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

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

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

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

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

  record Code(int value) {}
}
  • ObjectMapper는 jackson 라이브러리에서 제공하는 클래스로 객체와 JSON 간의 변환을 처리해준다. (객체 직렬화)

0개의 댓글