테스트 환경 통합 작업 - depromeet/Took-BE GitHub Wiki

📌 통합 테스트 환경 개요

테스트를 다음 5가지 범주로 구분합니다.

  1. 통합 테스트 (Integration Test)
    • 실제 스프링 컨텍스트를 로드하여 운영 환경과 유사하게 API 플로우를 검증합니다.
    • @SpringBootTest, @Transactional, @ActiveProfiles 등을 사용하며, 실제 HTTP 요청을 보내는 RestAssured 기반의 테스트를 수행합니다.
  2. 컨트롤러 테스트 (Controller Test)
    • MockMvc를 사용하여 컨트롤러 레이어만 테스트하며, 서비스 및 데이터 계층은 모킹(Mock) 처리합니다.
    • @WebMvcTest, @MockBean, MockMvc를 활용하여 빠른 단위 테스트를 수행합니다.
  3. 서비스(단위/Mock) 테스트
    • 비즈니스 로직 단위만 검증하며 외부 의존성은 모킹(Mock)합니다.
    • MockitoExtension(@ExtendWith(MockitoExtension.class))을 사용하고, @InjectMocks, @Mock 등을 활용합니다.
  4. Repository 테스트
    • JPA 관련 Bean만 로드하여 데이터 접근 계층을 검증합니다.
    • @DataJpaTest, @AutoConfigureTestDatabase, @ActiveProfiles를 사용합니다.
  5. POJO 테스트(도메인 테스트)
    • 엔티티 및 값 객체 등 단순 객체의 로직을 검증합니다.

각 범주마다 공통 베이스 클래스를 정의하여 테스트 코드의 일관성을 유지하고 중복을 줄입니다.


🛠 기본 베이스 클래스 (JUnit5 기준)

Integration Test (RestAssured 기반)

전체 스프링 컨텍스트를 로드하여 API 플로우를 검증하는 통합 테스트

✅ 코드 예시:

package com.evenly.blok.global.integration;

import static com.evenly.blok.global.common.constants.EnvironmentConstants.*;

import io.restassured.RestAssured; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.transaction.annotation.Transactional;

import com.fasterxml.jackson.databind.ObjectMapper;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ExtendWith(SpringExtension.class) @Transactional @ActiveProfiles(TEST_ENV) public abstract class IntegrationTest {

@LocalServerPort
protected int port;

@Autowired
protected ObjectMapper objectMapper;

@BeforeEach
public void setUp() {
    RestAssured.baseURI = "<http://localhost>";
    RestAssured.port = port;
}

}

어노테이션 설명 (RestAssured 기반 IntegrationTest)

  • @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    • 실제 내장 서버를 실행하여 API를 호출할 수 있도록 환경을 설정합니다.
    • RANDOM_PORT를 사용하면 동적 포트에서 애플리케이션이 실행되어 테스트 간 포트 충돌을 방지할 수 있습니다.
  • @ExtendWith(SpringExtension.class)
    • JUnit5에서 Spring의 테스트 컨텍스트를 활성화하여 스프링 관련 기능(빈 주입 등)을 사용할 수 있도록 합니다.
  • @Transactional
    • 각 테스트 실행 후 자동으로 데이터베이스 롤백을 수행하여 테스트 간 데이터 일관성을 유지합니다.
    • Spring Boot의 @Transactional은 기본적으로 rollback = true가 적용됩니다.
    • 주의: RestAssured를 사용하는 경우, 트랜잭션이 다른 쓰레드에서 실행될 수 있으므로 @Transactional이 적용되지 않을 수 있음.
      • 이 경우, @Rollback(true)를 사용하거나 테스트 데이터 정리를 직접 수행하는 방식을 고려해야 합니다.
  • @ActiveProfiles(TEST_ENV)
    • 테스트 실행 시 TEST_ENV 프로파일을 활성화하여 테스트용 설정(application-test.yml 등)을 적용합니다.
  • @LocalServerPort
    • 내장된 랜덤 포트를 주입하여 RestAssured.port로 설정합니다.
    • 이를 통해 실제 실행 중인 서버의 포트에서 API 요청을 보낼 수 있습니다.
  • RestAssured 설정 (@BeforeEach)
    • 테스트 실행 전에 RestAssured.baseURIport를 설정하여 모든 요청이 동적으로 할당된 포트에서 실행되도록 합니다.

Controller Test (MockMVC 기반)

컨트롤러 단위만 테스트하며 서비스 계층은 모킹(Mock)하는 테스트

✅ 코드 예시:

package com.evenly.blok.global.controller;

import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc;

import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired;

@WebMvcTest @ExtendWith(SpringExtension.class) public abstract class ControllerTest {

@Autowired
protected MockMvc mvc;

@Autowired
protected ObjectMapper objectMapper;

}

어노테이션 설명

  • @SpringBootTest
    • 전체 스프링 컨텍스트를 로드하여 실제 환경과 유사한 테스트를 수행합니다.
  • @AutoConfigureMockMvc
    • MockMvc 빈을 자동으로 구성하여 컨트롤러 레이어 테스트를 지원합니다.
  • @ExtendWith(SpringExtension.class)
    • JUnit5에서 Spring의 테스트 컨텍스트를 활성화하여 스프링 관련 기능을 사용할 수 있도록 합니다.
  • @Transactional
    • 테스트 실행 후 자동으로 데이터베이스 롤백을 수행하여 데이터 정합성을 유지합니다.
  • @ActiveProfiles(TEST_ENV)
    • 테스트 실행 시 TEST_ENV 프로파일을 활성화하여 환경별 설정을 적용합니다.
  • @Ignore
    • 이 클래스를 직접 실행하지 않도록 합니다.

🚀 ControllerTest vs IntegrationTest 비교

  MockMvc 기반 ControllerTest RestAssured 기반 IntegrationTest
테스트 대상 컨트롤러 단위 테스트 API 통합 테스트
서버 실행 여부 ❌ 서블릿 컨테이너 없음 ✅ 내장 서버 실행
Mocking 여부 ✅ @MockBean으로 Service 모킹 ❌ 실제 서버 실행
사용 프레임워크 MockMvc RestAssured
사용 목적 컨트롤러 로직 검증 전체 API 흐름 검증

왜 분리했는가?

  1. 속도
    • MockMvc 기반 ControllerTest는 서블릿 컨테이너를 실행하지 않기 때문에 빠르게 실행됩니다.
    • IntegrationTest는 실제 서버를 실행하므로 속도가 상대적으로 느립니다.
  2. 테스트 범위
    • ControllerTest는 컨트롤러 단위만 검증하며, 서비스와 데이터 계층을 @MockBean으로 모킹합니다.
    • IntegrationTest는 실제 애플리케이션을 실행하고, 데이터베이스까지 포함하여 엔드투엔드(E2E) 테스트를 수행합니다.
  3. 실제 환경과의 유사성
    • IntegrationTestSpring Security, 필터, 트랜잭션 처리 등 모든 레이어를 포함하므로 실제 운영 환경과 가장 유사한 테스트를 수행할 수 있습니다.
    • ControllerTest컨트롤러 레이어만 검증하여 빠르고 가벼운 테스트가 가능합니다.

서비스(Mock) 테스트 베이스 (MockTest)

비즈니스 로직 단위만 검증하며 외부 의존성을 모킹(Mock)하는 테스트

✅ 코드 예시:

package com.evenly.blok.global.service;

import static com.evenly.blok.global.common.constants.EnvironmentConstants.*;

import org.junit.Ignore; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.context.ActiveProfiles;

@ExtendWith(MockitoExtension.class) @ActiveProfiles(TEST_ENV) @Ignore public abstract class MockTest { }

어노테이션 설명

  • @ExtendWith(MockitoExtension.class)
    • Mockito 기반의 단위 테스트를 지원하며, @Mock@InjectMocks를 사용하여 모킹을 수행할 수 있도록 합니다.
  • @ActiveProfiles(TEST_ENV)
    • 테스트 환경을 TEST_ENV 프로파일로 설정합니다.
  • @Ignore
    • 이 클래스를 직접 실행하지 않도록 합니다.

Repository 테스트 베이스 (RepositoryTest)

JPA 관련 Bean만 로드하여 데이터 접근 계층을 검증하는 테스트

✅ 코드 예시:

package com.evenly.blok.global.repository;

import static com.evenly.blok.global.common.constants.EnvironmentConstants.*;

import org.junit.Ignore; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit.jupiter.SpringExtension;

@DataJpaTest @ExtendWith(SpringExtension.class) @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @ActiveProfiles(TEST_ENV) @Ignore public abstract class RepositoryTest { }

어노테이션 설명

  • @DataJpaTest
    • JPA 관련 Bean만 로드하여 데이터베이스 접근 계층을 검증하는 테스트 환경을 구성합니다.
  • @ExtendWith(SpringExtension.class)
    • Spring의 테스트 컨텍스트를 활성화합니다.
  • @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
    • 기본적으로 내장된 데이터베이스(H2)를 사용하지 않고, 실제 테스트 데이터베이스를 사용하도록 설정합니다.
  • @ActiveProfiles(TEST_ENV)
    • 테스트 실행 시 TEST_ENV 프로파일을 활성화합니다.
  • @Ignore
    • 이 클래스를 직접 실행하지 않도록 합니다.

POJO 테스트 (도메인 테스트)

엔티티 및 값 객체 등 단순 객체의 로직을 검증하는 테스트

별도의 베이스 클래스 없이 개별 테스트 클래스로 작성합니다.

✅ 코드 예시:

package com.evenly.blok;

import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test;

class MemberTest {

@Test
void 유저_이름_변경() {
    // given
    String newName = "newName";
    Member member = new Member("name");

    // when
    member.update(newName);

    // then
    Assertions.assertThat(member.getName()).isEqualTo(newName);
}

}


📌 각 테스트 유형별 실제 예제

통합 테스트 – HealthControllerIntegrationTest (RestAssured)

package com.evenly.blok.global.health;

import static io.restassured.RestAssured.; import static org.hamcrest.Matchers.;

import org.junit.jupiter.api.Test;

import com.evenly.blok.global.integration.IntegrationTest;

class HealthControllerIntegrationTest extends IntegrationTest {

@Test
void Health_Check_통합테스트_성공() {
    given().log().all()
        .when()
        .get("/api/health")
        .then()
        .log().all()
        .statusCode(200);
}

}

컨트롤러 테스트 – HealthControllerTest (MockMvc)

package com.evenly.blok.global.health;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.;

import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.ResultActions;

import com.evenly.blok.global.integration.IntegrationTest;

class HealthControllerTest extends IntegrationTest {

@Test
void Health_Check_통합테스트_성공() throws Exception {
    // given, when
    ResultActions resultActions = requestHealthCheck();

    // then
    resultActions.andExpect(status().isOk());
}

private ResultActions requestHealthCheck() throws Exception {
    return mvc.perform(get("/api/health")
        .contentType(MediaType.APPLICATION_JSON));
}

}

서비스(Mock) 테스트 – MemberServiceTest

package com.evenly.blok;

import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify;

import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock;

import com.evenly.blok.global.service.MockTest;

class MemberServiceTest extends MockTest {

@InjectMocks
private MemberService memberService;

@Mock
private MemberRepo memberRepo;

private Member member;

@BeforeEach
void setUp() {
    member = new Member("name");
}

@Test
void 저장_성공() {
    // given
    given(memberRepo.save(any())).willReturn(member);

    // when
    memberService.save(member);

    // then
    verify(memberRepo, times(1)).save(any(Member.class));
}

}

Repository 테스트 – MemberRepoTest

package com.evenly.blok;

import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired;

import com.evenly.blok.global.repository.RepositoryTest;

class MemberRepoTest extends RepositoryTest {

@Autowired
private MemberRepo memberRepo;

@Test
void 유저_저장_성공() {
    // given
    Member member = new Member("name");

    // when
    member = memberRepo.save(member);

    // then
    Assertions.assertThat(member).isNotNull();
}

}

✅ POJO 테스트 - MemberTest

package com.evenly.blok;

import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test;

class MemberTest {

@Test
void 유저_이름_변경() {
    // given
    String newName = "newName";
    Member member = new Member("name");

    // when
    member.update(newName);

    // then
    Assertions.assertThat(member.getName()).isEqualTo(newName);
}

}


✅ 결론

위와 같이 각 테스트 유형별로 공통 베이스 클래스를 정의하여 일관성 유지, 중복 제거, 유지보수 편의성을 높일 수 있습니다.

각 테스트 유형에 적합한 어노테이션을 사용하여 최적화된 테스트 환경을 구성합니다.

⚠️ **GitHub.com Fallback** ⚠️