테스트 환경 통합 작업 - depromeet/Took-BE GitHub Wiki
테스트를 다음 5가지 범주로 구분합니다.
-
통합 테스트 (Integration Test)
- 실제 스프링 컨텍스트를 로드하여 운영 환경과 유사하게 API 플로우를 검증합니다.
-
@SpringBootTest
,@Transactional
,@ActiveProfiles
등을 사용하며, 실제 HTTP 요청을 보내는 RestAssured 기반의 테스트를 수행합니다.
-
컨트롤러 테스트 (Controller Test)
-
MockMvc
를 사용하여 컨트롤러 레이어만 테스트하며, 서비스 및 데이터 계층은 모킹(Mock) 처리합니다. -
@WebMvcTest
,@MockBean
,MockMvc
를 활용하여 빠른 단위 테스트를 수행합니다.
-
-
서비스(단위/Mock) 테스트
- 비즈니스 로직 단위만 검증하며 외부 의존성은 모킹(Mock)합니다.
-
MockitoExtension(@ExtendWith(MockitoExtension.class))
을 사용하고,@InjectMocks
,@Mock
등을 활용합니다.
-
Repository 테스트
- JPA 관련 Bean만 로드하여 데이터 접근 계층을 검증합니다.
-
@DataJpaTest
,@AutoConfigureTestDatabase
,@ActiveProfiles
를 사용합니다.
-
POJO 테스트(도메인 테스트)
- 엔티티 및 값 객체 등 단순 객체의 로직을 검증합니다.
각 범주마다 공통 베이스 클래스를 정의하여 테스트 코드의 일관성을 유지하고 중복을 줄입니다.
전체 스프링 컨텍스트를 로드하여 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;
}
}
-
@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.baseURI
와port
를 설정하여 모든 요청이 동적으로 할당된 포트에서 실행되도록 합니다.
-
테스트 실행 전에
컨트롤러 단위만 테스트하며 서비스 계층은 모킹(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
- 이 클래스를 직접 실행하지 않도록 합니다.
MockMvc 기반 ControllerTest | RestAssured 기반 IntegrationTest | |
---|---|---|
테스트 대상 | 컨트롤러 단위 테스트 | API 통합 테스트 |
서버 실행 여부 | ❌ 서블릿 컨테이너 없음 | ✅ 내장 서버 실행 |
Mocking 여부 | ✅ @MockBean으로 Service 모킹 | ❌ 실제 서버 실행 |
사용 프레임워크 | MockMvc | RestAssured |
사용 목적 | 컨트롤러 로직 검증 | 전체 API 흐름 검증 |
-
속도
-
MockMvc
기반ControllerTest
는 서블릿 컨테이너를 실행하지 않기 때문에 빠르게 실행됩니다. -
IntegrationTest
는 실제 서버를 실행하므로 속도가 상대적으로 느립니다.
-
-
테스트 범위
-
ControllerTest
는 컨트롤러 단위만 검증하며, 서비스와 데이터 계층을@MockBean
으로 모킹합니다. -
IntegrationTest
는 실제 애플리케이션을 실행하고, 데이터베이스까지 포함하여 엔드투엔드(E2E) 테스트를 수행합니다.
-
-
실제 환경과의 유사성
-
IntegrationTest
는 Spring Security, 필터, 트랜잭션 처리 등 모든 레이어를 포함하므로 실제 운영 환경과 가장 유사한 테스트를 수행할 수 있습니다. -
ControllerTest
는 컨트롤러 레이어만 검증하여 빠르고 가벼운 테스트가 가능합니다.
-
비즈니스 로직 단위만 검증하며 외부 의존성을 모킹(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
를 사용하여 모킹을 수행할 수 있도록 합니다.
- Mockito 기반의 단위 테스트를 지원하며,
-
@ActiveProfiles(TEST_ENV)
- 테스트 환경을
TEST_ENV
프로파일로 설정합니다.
- 테스트 환경을
-
@Ignore
- 이 클래스를 직접 실행하지 않도록 합니다.
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
- 이 클래스를 직접 실행하지 않도록 합니다.
엔티티 및 값 객체 등 단순 객체의 로직을 검증하는 테스트
별도의 베이스 클래스 없이 개별 테스트 클래스로 작성합니다.
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);
}
}
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);
}
}
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));
}
}
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));
}
}
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();
}
}
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);
}
}
위와 같이 각 테스트 유형별로 공통 베이스 클래스를 정의하여 일관성 유지, 중복 제거, 유지보수 편의성을 높일 수 있습니다.
각 테스트 유형에 적합한 어노테이션을 사용하여 최적화된 테스트 환경을 구성합니다.