Spring and Spring Boot/How to use Spring Boot

Spring Boot : 스프링부트 프로젝트 만들기 시리즈 03. 테스트 (JUnit, Unit test, Integration test, Mocking, MockMvc, @MockBean, Stub)

마이트너 2025. 3. 5. 11:42

목차

 

테스트 기초편 : 스프링부트 테스트 환경 설정

테스트 기초편 : 스프링부트의 다양한 테스트 기법 (단위 테스트, 통합 테스트, 컨트롤러 테스트)
테스트 기초편 : 기본 어노테이션 살펴보기

 

테스트 실전편 첫 번째 : 복잡한 비즈니스 로직을 위한 테스트 전략 (Mockito, Mocking, MockMvc, @MockBean, Stub)

테스트 실전편 두 번째 : 데이터베이스와 통합 테스트

테스트 실전편 세 번째 : REST API 테스트와 MockMvc
테스트 실전편 네 번째 : 모의 객체(Mock Objects)와 의존성 주입

테스트 실전편 다섯 번째 : 예외 처리 및 경계 조건 테스트

테스트 실전편 여섯 번째 : 성능 테스트 및 최적화

테스트 실전편 일곱 번째 : CI/CD와 통합된 테스트 파이프라인 구성

테스트 실전편 여덟 번째 : 테스트 코드의 유지보수성


테스트 번외편 : 유의해야 할 점


테스트 기초편 : 스프링부트 테스트 환경 설정

스프링부트에서는 Spring Test 모듈을 제공하여 다양한 테스트를 지원합니다. 스프링부트에서 테스트를 실행하려면 몇 가지 설정이 필요한데, 이는 build.gradle 또는 pom.xml에 테스트 관련 의존성을 추가할 수 있습니다.

  • spring-boot-starter-test 의존성 : 스프링 테스트, JUnit, Mockito, Hamcrest, AssertJ 등 다양한 테스트 관련 라이브러리를 포함하고 있는, 스프링부트 테스트의 가장 기초이자 핵심인 의존성입니다. 놓치지 말고 추가하시길 바랍니다.
  • mockito-junit-jupiter 의존성 : Unit 5와 Mockito를 통합하기 위한 라이브러리입니다. 이 의존성을 추가하면 JUnit 5에서 Mockito를 더욱 쉽게 사용할 수 있습니다. MockitoExtension을 사용하여 Mockito의 Mock 객체를 JUnit 5 테스트에서 자동으로 초기화하고 관리할 수 있게 됩니다.
//gradle일 경우 : build.gradle

dependencies {
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.mockito:mockito-junit-jupiter:5.2.0' // 최신 버전 확인 후 적용
}
//maven일 경우 : pom.xml

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.1.0</version> <!-- 최신 버전은 확인 후 적용 -->
    <scope>test</scope>
</dependency>

테스트 기초편 : 스프링부트의 다양한 테스트 기법

 

1. 단위 테스트(Unit Test)

단위 테스트는 애플리케이션의 각 단위(클래스, 메서드)가 독립적으로 동작하는지 확인하는 테스트입니다. 즉, 단위 테스트는 개별적인 코드 조각(주로 함수나 메서드)을 테스트하는 데 초점을 맞춥니다. 이 테스트는 코드가 특정 동작을 제대로 수행하는지 검증하는 것에 목적을 둡니다. 자세한 이야기는 뒤에서 할테지만, 외부 의존성이나 DB와 상호작용하지 않도록 Mocking 기법을 사용하여 테스트한다는 점은 기억해둡시다.

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

public class MyServiceTest {

    @Test
    public void testServiceMethod() {
        // 1. Mock 객체 생성
        MyRepository mockRepository = mock(MyRepository.class);
        
        // 2. when ~ then 구문을 사용해 동작 설정
        when(mockRepository.getData()).thenReturn("Mocked Data");

        // 3. 실제 서비스 객체 생성
        MyService myService = new MyService(mockRepository);

        // 4. 서비스 메서드 호출 및 검증
        String result = myService.getDataFromRepository();
        assertEquals("Mocked Data", result);
    }
}
예시 설명

when(mockRepository.getData()).thenReturn("Mocked Data");는 getData() 메서드가 호출되었을 때 "Mocked Data"를 반환하도록 설정합니다.

MyService의 getDataFromRepository() 메서드를 호출하면, mockRepository.getData()는 실제 데이터베이스나 외부 서비스와 연결된 게 아니라 Mock 객체에서 설정한 "Mocked Data"를 반환합니다.

 

 

2. 통합 테스트(Integration Test)

통합 테스트는 말 그대로 애플리케이션의 여러 컴포넌트가 잘 연결되고 상호작용하는지 확인하는 통합적인 테스트합니다. 데이터베이스, 외부 API 등 실제 시스템과 연동되므로 더 실질적인 테스트가 가능하므로, 단위 테스트와 함께 필수적인 테스트입니다.

@SpringBootTest
public class OrderServiceTest {
  
    @Autowired
    private OrderService orderService;

    @Test
    void testPlaceOrder() {
        orderService.placeOrder(new Order());
    }
}
통합 테스트에서 Mock을 권장하지 않는 이유

1. 실제 시스템 동작 확인
: 통합 테스트는 시스템이 실제 환경에서 어떻게 동작하는지를 확인하는 것이므로, 외부 시스템이나 서비스와의 연동을 테스트해야 합니다. Mock을 사용하면 실제 외부 시스템과의 상호작용을 테스트하지 않게 됩니다.

2. 실제 데이터베이스 사용
: 데이터베이스와의 연동, API 호출, 외부 서비스와의 상호작용 등이 실제로 어떻게 이루어지는지 확인해야 하므로, Mock을 사용하지 않고 실제 환경에서 테스트하는 것이 중요합니다.

3. 종속성 있는 실제 구성 요소 테스트
: 외부 서비스나 데이터베이스 같은 구성 요소와의 실제 연동을 테스트함으로써, 서비스의 상호작용에서 발생할 수 있는 문제를 확인할 수 있습니다.
그럼 언제 Mock을 사용할까요?
통합 테스트에서 Mock을 사용하는 경우는 다음과 같습니다.

1. 외부 시스템과의 연동이 필요한 경우
: 예를 들어, API 호출, 외부 데이터베이스, 파일 시스템 등과의 연동이 필요한 경우, 실제 서비스가 응답을 제공하지 않거나 느릴 때 Mock을 사용하여 테스트의 속도를 높이거나 의도적인 응답을 설정할 수 있습니다.
: 예를 들어, 외부 서비스가 느리거나 실패하는 경우 이를 Mock하여 특정 에러 상황을 테스트할 수 있습니다.

2. 비즈니스 로직 테스트와 시스템 간 상호작용을 분리해야 할 때
: 데이터베이스나 외부 API와 연동되는 복잡한 비즈니스 로직을 테스트할 때, 실제 연동 없이 Mock을 사용하여 비즈니스 로직만 집중적으로 테스트할 수 있습니다.

3. 테스트 환경에서 외부 서비스에 의존하지 않으려는 경우
: 외부 서비스나 데이터베이스가 테스트 환경에서 사용할 수 없는 경우 (예: 네트워크 장애, 외부 시스템 제한) Mock을 사용하여 테스트 환경을 제어할 수 있습니다.

아래 예시에서는 @MockBean을 사용하여 외부 API를 Mock으로 설정하고, 해당 API가 호출되었을 때 특정 응답을 반환하도록 설정했습니다. 이 경우, 외부 API가 실제로 호출되지 않고, Mock 데이터만 사용하여 테스트를 진행합니다.
@SpringBootTest
class UserServiceIntegrationTest {

    @MockBean
    private ExternalApiService externalApiService; // 외부 API 서비스 Mock

    @Autowired
    private UserService userService;

    @Test
    void testFetchUserData() {
        // Mock 응답 설정
        when(externalApiService.getUserData(anyString())).thenReturn(new UserData("John", "john@example.com"));
        
        // 실제 서비스 메서드 호출
        User user = userService.fetchUserData("userId");
        
        assertEquals("John", user.getName());
    }
}​

 

 

3. 컨트롤러 테스트(Web Test)

컨트롤러 테스트는 웹 계층을 테스트합니다. MockMvc를 사용하여 HTTP 요청과 응답을 모킹(Mock)하고, 컨트롤러가 예상대로 동작하는지 확인합니다.

@WebMvcTest
public class MyControllerTest {
  
    @Autowired
    private MockMvc mockMvc;

    @Test
    void testController() throws Exception {
        mockMvc.perform(get("/items"))
               .andExpect(status().isOk());
    }
}

테스트 기초편 : 기본 어노테이션 살펴보기

참고자료 : 테스트에 사용하는 어노테이션 더 자세히보기

 

 

1. 스프링부트에서의 JUnit 사용

스프링부트 테스트는 JUnit을 기반으로 동작합니다. 기본적으로 JUnit 5를 사용하지만, JUnit 4도 사용 가능합니다. 저희는 JUnit5 기준으로 학습해볼 것입니다. 아래는 JUnit 5를 활용한 간단한 테스트 예시입니다.

  • @Test 어노테이션 : 사용하여 테스트 메서드를 정의합니다. 
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {

    @Test
    void addNumbers() {
        int result = 2 + 3;
        assertEquals(5, result);
    }
}

 

 

2. 스프링부트에서 제공하는 어노테이션

  • @SpringBootTest : 실제 스프링 애플리케이션을 실행하고 테스트합니다. 애플리케이션 컨텍스트를 로드하고, 전체적인 통합 테스트를 수행합니다. 
@SpringBootTest 
public class MyServiceTest { 

	@Autowired private MyService myService; 
    
    @Test public void testService() { 
    	assertNotNull(myService); } 
    }

 

  • @SpringBootTest를 사용하지 않는 경우 (순수 JUnit) : 애플리케이션의 특정 부분만 테스트 즉, 단위 테스트를 할 때는 @SpringBootTest 대신 다른 JUnit 테스트 어노테이션을 사용할 수 있습니다.
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;

class MyServiceTest {

    @Test
    void testServiceMethod() {
        // Mock 객체 생성
        MyRepository myRepository = mock(MyRepository.class);

        // Mock 객체가 호출될 때 반환할 값 설정
        when(myRepository.getData()).thenReturn("Mocked Data");

        // 서비스 객체 생성
        MyService myService = new MyService(myRepository);

        // 메서드 호출 및 결과 검증
        String result = myService.getDataFromRepository();
        assertEquals("Mocked Data", result);
    }
}

 

  • @SpringBootTest를 사용하지 않는 경우(Mockito를 이용한 테스트) : @SpringBootTest 없이도 Mockito를 사용하여 단위 테스트를 작성할 수 있습니다. Mockito를 사용하면 클래스 간 의존성을 Mock 객체로 대체하여, 실제 의존성 없이 특정 기능만 테스트할 수 있습니다.
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.InjectMocks;
import static org.mockito.Mockito.*;

class MyServiceTest {

    @Mock
    private MyRepository myRepository;  // Mockito로 Mock 객체 생성

    @InjectMocks
    private MyService myService;  // Mock 객체를 주입받는 MyService 객체

    @Test
    void testServiceMethod() {
        // Mock 객체 동작 설정
        when(myRepository.getData()).thenReturn("Mocked Data");

        // 테스트 실행
        String result = myService.getDataFromRepository();

        // 결과 검증
        assertEquals("Mocked Data", result);
    }
}

 

  • @SpringBootTest를 사용하지 않는 경우(JUnit 5의 @ExtendWith + Mockito) : Spring Boot 없이 JUnit 5에서 Mockito를 사용할 때는 @ExtendWith(MockitoExtension.class)를 사용하여 Mockito 확장을 활성화할 수 있습니다. 이 방법은 @RunWith(MockitoJUnitRunner.class)와 비슷하지만 JUnit 5에 맞게 설정됩니다. 
    • 그렇다면 확장을 사용하지 않으면 어떨까요?
      • 테스트 실행 전후에 추가적인 동작(예: 리소스 초기화, 외부 시스템과의 연결 등)을 제어할 방법이 제한됩니다. 하지만 @ExtendWith를 사용하면 beforeEach, afterEach 메서드를 통해 테스트 메서드 전후의 작업을 제어할 수 있습니다.
    • 확장을 사용하면 뭐가 좋을까요?
      • @ExtendWith를 사용하여 확장 기능을 추가하면, 테스트 메서드의 실행을 조건부로 제어할 수 있습니다. 예를 들어, 특정 조건에 따라 테스트를 실행하거나, 특정 환경에서만 테스트가 실행되도록 설정할 수 있습니다.
      • 외부 라이브러리나 프레임워크의 확장 기능을 손쉽게 통합할 수 있습니다. 예를 들어, JUnit 5에서 제공하는 확장 외에도, Mockito나 Spring Test와 같은 외부 라이브러리의 기능을 통합할 때 유용합니다.
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)  // MockitoExtension을 통해 Mockito 활성화
public class MyServiceTest {

    @Mock
    private MyRepository myRepository;  // Mock 객체

    @InjectMocks
    private MyService myService;  // Mock 객체를 주입받는 MyService 객체

    @Test
    void testServiceMethod() {
        // Mock 객체가 호출될 때 반환할 값 설정
        when(myRepository.getData()).thenReturn("Mocked Data");

        // 메서드 호출 및 결과 검증
        String result = myService.getDataFromRepository();
        assertEquals("Mocked Data", result);
    }
}

 

  • @WebMvcTest : 웹 계층만 테스트합니다. 주로 컨트롤러를 테스트할 때 사용되며, 서비스나 레포지토리 등 다른 계층은 Mock 객체로 대체할 수 있습니다. 서비스나 레포지토리 등에 대한 자세한 코드는 실전편에서 봅시다.
@WebMvcTest 
public class MyControllerTest { 

    @Autowired 
    private MockMvc mockMvc; 
    
    @Test 
    public void testController() throws Exception { 
    	mockMvc.perform(get("/api/items")) 
        	.andExpect(status().isOk()) 
            .andExpect(jsonPath("$.length()").value(2)); 
    } 
}

테스트 실전편 첫 번째 : 복잡한 비즈니스 로직을 위한 테스트 전략

이번에는 실제로 사용하는 테스트 코드를 살펴보며 이해해봅시다. 비즈니스 로직이 복잡해지면 단순히 엔티티를 저장하는 Repository 테스트를 넘어서, 서비스 계층이나 비즈니스 로직에 대한 테스트가 필요합니다. 이때, Mocking, Stub, Mockito와 같은 기법이 유용하게 사용되며, 이를 사용하여 어떻게 테스트를 구조화할지 고려하게 됩니다. 이번 편에서는 복잡한 비즈니스 로직을 테스트하기 위한 전략을 다뤄보겠습니다. 실제 코드 예제를 통해 테스트 작성 방법을 살펴보며, 다양한 기법들을 함께 적용해봅시다.

 

 

1. 서비스 계층 테스트의 필요성

애플리케이션의 복잡한 비즈니스 로직은 일반적으로 서비스 계층에서 처리됩니다. 예를 들어, 상품의 가격을 계산하거나, 주문 처리를 할 때 여러 조건을 따져야 할 경우, 비즈니스 규칙을 구현한 서비스 계층의 테스트가 중요합니다. 단위 테스트(Unit Test)는 각 서비스의 메서드가 예상대로 동작하는지 확인하는 데 사용됩니다.

 

서비스 계층의 비즈니스 로직을 테스트할 때는 외부 의존성Mocking하여 테스트의 범위를 서비스 로직에만 집중할 수 있도록 해야 합니다. 예를 들어, 외부 API 호출, 데이터베이스 접근, 혹은 다른 서비스의 의존성 등을 Mock 처리하여 실제 환경에 영향을 미치지 않도록 합니다.

 

 

2. 테스트 범위를 서비스 로직에만 집중할 수 있게 하는 방법 : Mocking한다는 것의 의미
외부 의존성을 Mocking한다는 것은, 테스트를 작성할 때 외부 시스템이나 다른 컴포넌트와의 의존 관계를 실제로 호출하거나 연결하는 대신, 그 의존성을 모방(즉, mock)하여 테스트하는 것을 의미합니다. 이는 주로 테스트 환경에서 외부 서비스나 데이터베이스, 네트워크 호출, 파일 시스템과 같은 리소스에 의존하지 않고 독립적으로 코드를 테스트하기 위해 사용됩니다.

  • 외부 API 호출 Mocking : 예를 들어, 어떤 시스템이 외부 API에 데이터를 요청하고, 그 응답을 처리하는 기능이 있다고 합시다. 테스트 환경에서는 실제로 외부 API를 호출할 필요 없이, API의 응답을 모방하는 mock 객체를 만들어 테스트할 수 있습니다. 이렇게 하면 네트워크 지연이나 외부 시스템의 상태에 영향을 받지 않고 안정적인 테스트를 할 수 있습니다.
  • 데이터베이스 Mocking : 데이터베이스에 의존하는 코드를 테스트할 때, 실제 데이터베이스와의 연결 없이 mock 객체를 사용하여 데이터베이스 쿼리 결과를 시뮬레이션할 수 있습니다. 이 경우 테스트 중에 데이터베이스의 상태가 변하거나 외부 서비스에 의해 영향을 받을 걱정 없이 특정 로직만 테스트할 수 있습니다.
Mocking의 장단점

장점
1. 속도 : 실제 외부 시스템과 연결되는 시간을 절약할 수 있어 테스트 속도가 빨라집니다.
2. 독립성 : 외부 시스템이나 의존성이 문제가 생겨도 테스트가 영향을 받지 않도록 독립적인 테스트를 할 수 있습니다.
3. 일관성 : 외부 시스템의 상태에 의존하지 않으므로 테스트 결과가 일관되게 유지됩니다.
4. 에러 핸들링 테스트 : 실제 외부 시스템이 다운되거나 응답하지 않는 상황을 mock을 통해 시뮬레이션하여, 시스템이 이런 에러를 어떻게 처리하는지 테스트할 수 있습니다.

단점 : Mocking은 테스트를 더 쉽고, 빠르며, 견고하게 만들어 줍니다. 다만, 지나치게 많은 mock 객체를 사용하면 실제 구현과 테스트 간의 차이가 생길 수 있으므로, 적절히 사용하는 것이 중요하다고 합니다.

 

 

3. 테스트에 사용하는 외부 라이브러리 : Mockito를 사용한 Mocking하는 방법 알아보기

Mockito는 객체를 Mock(가짜 객체)로 만들어 테스트할 때 사용하는 라이브러리입니다. @Mock, @InjectMocks와 같은 어노테이션을 활용하여 의존성 주입을 쉽게 할 수 있으며, 외부 시스템과의 의존성은 Mock 객체로 대체할 수 있습니다.

 

EX. 상품 가격 계산 서비스

예를 들어, 상품의 가격을 계산하는 서비스가 있다고 가정해봅시다. 이 서비스는 외부 세금 계산 서비스를 의존하고 있다고 할 때, 세금 계산 서비스를 Mocking하여 실제 서비스 로직만 테스트할 수 있습니다.

@Service
public class ProductService {
    private final TaxService taxService;

    @Autowired
    public ProductService(TaxService taxService) {
        this.taxService = taxService;
    }

    public double calculatePrice(Product product) {
        double basePrice = product.getBasePrice();
        double taxAmount = taxService.calculateTax(basePrice);  // 외부 서비스 의존
        return basePrice + taxAmount;
    }
}

 

이제 ProductService의 calculatePrice 메서드를 테스트해봅시다. 단, 실제 TaxService는 호출하지 않고 Mock 객체를 사용하도록 합시다. 이렇게 하면 taxService.calculateTax 메서드는 실제로 호출되지 않고, Mock된 객체를 통해 반환값을 제어할 수 있습니다. 즉, Mocking을 이용하면 외부 서비스나 복잡한 의존성과 관계없이 서비스 로직만을 테스트할 수 있는 것입니다.

@ExtendWith(MockitoExtension.class)
class ProductServiceTest {

    @Mock
    private TaxService taxService;  // TaxService는 Mock 객체로 대체

    @InjectMocks
    private ProductService productService;  // ProductService에 Mock 객체 주입

    @Test
    void calculatePrice_withMockedTaxService() {
        // Given
        Product product = new Product(100.0);  // 기본 가격 100원
        when(taxService.calculateTax(100.0)).thenReturn(10.0);  // 세금은 10원으로 Mocking

        // When
        double price = productService.calculatePrice(product);

        // Then
        assertEquals(110.0, price);  // 가격 + 세금 = 110
    }
}

 

 

4. 스프링 테스트 라이브러리에서 제공하는 Mock 객체와 관련된 기능 : MockMvc와 @MockBean

MockMvc@MockBeanMockito 라이브러리와 밀접하게 관련이 있지만, MockMvc@MockBean은 각각 다른 역할을 수행하는 기능입니다. 각각 살펴봅시다.

  • MockMvc 활용한 웹 계층 테스트 : MockMvc는 실제 HTTP 요청을 시뮬레이션하여 실제 웹 서버를 실행하지 않고도 컨트롤러나 웹 계층의 동작을 테스트할 수 있게 해주는 스프링 테스트 라이브러리의 기능입니다. MockMvc를 사용하면 HTTP GET, POST, PUT, DELETE 요청을 보낼 수 있으며, 응답을 검증할 수 있습니다. 이러한 MockMvc는 스프링에서 제공하는 기능으로, Mockito와는 별개입니다. 
@Autowired
private MockMvc mockMvc;

@Test
void testGetItems() throws Exception {
    mockMvc.perform(get("/items"))
           .andExpect(status().isOk())
           .andExpect(jsonPath("$.length()").value(2));
}

MockMvc와 Mockito의 관계


MockMvc 자체는 Mockito 라이브러리와 직접적인 관계는 없지만, Mockito를 활용하여 의존성 객체를 Mocking하고 MockMvc로 테스트할 때 함께 사용할 수 있습니다. Mockito는 객체를 Mocking(가짜 객체로 대체)하는 라이브러리로, MockMvc의 테스트 환경을 설정할 때 외부 의존성을 대체하는 데 사용될 수 있습니다. 예를 들어, MockMvc 테스트에서 Service나 Repository와 같은 외부 의존성을 Mockito로 Mock 처리하여 테스트할 수 있습니다.

 

  • @MockBean과 @Autowired를 활용한 의존성 주입 : 스프링부트 테스트에서는 @MockBean과 @Autowired를 사용하여 테스트 대상 클래스에 필요한 의존성을 주입할 수 있습니다. @MockBean은 테스트 중에 Mock 객체를 주입하고, @Autowired는 실제 빈을 주입한다는 차이가 있습니다. 테스트 시에 Mocking 사용 여부에 따라 주입 어노테이션을 정해야 합니다.
@MockBean
private ItemRepository itemRepository;  // Mocking ItemRepository

@Autowired
private ItemService itemService;  // 실제 서비스
@MockBean과 Mockito의 관계

@MockBean은 스프링 부트에서 제공하는 어노테이션으로, Mockito를 활용하여 Mock 객체를 스프링의 애플리케이션 컨텍스트에 주입하는 역할을 합니다. 이를 통해 테스트 중에 실제 객체 대신 Mock 객체를 주입할 수 있습니다. Mockito의 Mock 객체를 쉽게 스프링의 빈으로 등록할 수 있기 때문에 Mockito와 밀접한 관계가 있습니다.

@MockBean은 Mockito를 기반으로 작동하지만, 스프링 부트의 테스트 환경에서 사용되는 스프링 특화 기능입니다. 이를 사용하면 Mockito로 만든 Mock 객체가 스프링 애플리케이션 컨텍스트에 통합되어 실제 서비스에서 사용되는 것처럼 작동할 수 있습니다. 즉, 스프링의 의존성 주입 기능을 활용하여 Mock 객체를 서비스나 컨트롤러에 주입할 수 있게 됩니다.

 

 

5. 통합 테스트와 Mock 객체의 활용

통합 테스트(Integration Test)에서는 실제 서비스들이 상호작용하는 방식으로 테스트가 필요합니다. 하지만 데이터베이스나 외부 API 호출을 포함한 테스트는 시간 소모적이고 환경에 의존적일 수 있으므로, 마찬가지로 Mock을 사용하여 외부 의존성을 대체하는 것이 유용합니다. 아래와 같이 @MockBean을 사용하여 Spring 환경에서 외부 의존성을 Mock 처리할 수 있습니다. 이 방식은 통합 테스트에서 실제 데이터베이스 외부 API 대신 Mock 객체를 활용하여 테스트를 진행할 수 있도록 해줍니다.

@ExtendWith(SpringExtension.class)
@SpringBootTest
public class OrderServiceTest {

    @MockBean
    private PaymentService paymentService;  // 외부 결제 서비스 Mock

    @Autowired
    private OrderService orderService;  // 실제 서비스 클래스는 SpringContext에서 주입

    @Test
    void processOrder_withMockedPaymentService() {
        // Given
        Order order = new Order(100.0);
        when(paymentService.processPayment(order)).thenReturn(true);  // 결제 성공을 Mocking

        // When
        boolean result = orderService.processOrder(order);

        // Then
        assertTrue(result);  // 결제 성공시 주문이 처리됨
    }
}

 

 

6. 의존성을 제어하는 또 다른 방법 : Stub을 사용한 테스트의 반환값 제어

Stub는 Mocking과 같은 맥락에서 의존성을 제어하는 방법으로, 테스트에서 반환값을 지정해줍니다. Mocking과 Stub을 사용한 의존성 제어가 테스트에서 어떻게 다르게 활용되는지 알아봅시다.

 

Mockito 라이브러리의 when(...).thenReturn(...)과 유사하지만, 복잡한 반환값을 설정하거나 예외를 던지기 위해 Stub가 유리할 수 있습니다. Mock과 Stub의 차이는 주로 동작의 설정 방법에 있으며, Stub은 주로 정적인 반환값을 설정하는 데 사용됩니다.

// Stub을 사용한 예시
when(taxService.calculateTax(100.0)).thenReturn(20.0);  // 세금은 20으로 설정

 

 

7. 복잡한 비즈니스 로직을 위한 테스트 전략 결론 : Mocking과 Stub 

지금까지 복잡한 비즈니스 로직을 테스트할 때는 MockingStub을 적극적으로 활용하여 외부 의존성을 제어하고, 실제 서비스 로직에만 집중할 수 있다는 사실을 알게 되었습니다. Mockito와 MockBean 등을 사용하면, 실제 환경에서의 동작을 모방하면서도 빠르고 효율적인 단위 테스트통합 테스트를 수행할 수 있습니다. 테스트 전략을 잘 세우면, 애플리케이션의 품질을 높이고, 유지보수성이 뛰어난 코드베이스를 구축할 수 있다는 점을 생각하며 다음 주제로 넘어가봅시다.

 


테스트 실전편 두 번째 : 데이터베이스와 통합 테스트

데이터베이스와의 통합 테스트는 애플리케이션에서 매우 중요한 역할을 합니다. 실제 시스템에서 데이터베이스와 상호작용하는 로직이 예상대로 동작하는지 검증하는 과정은 시스템의 품질을 보장하는 핵심 요소입니다. 이 글에서는 데이터베이스와의 통합 테스트에 대해 다루며, 실제 테스트 코드와 함께 어떻게 효과적으로 데이터베이스와의 상호작용을 검증할 수 있는지 살펴보겠습니다.

 

 

1. 왜 데이터베이스와의 통합 테스트가 중요한가?

  • 실제 데이터베이스의 동작 검증 : 애플리케이션의 비즈니스 로직이 데이터베이스와 연동될 때, 데이터가 어떻게 저장되고 조회되는지 확인하는 것이 중요합니다. 데이터베이스 연결, 트랜잭션, 쿼리 최적화 등 여러 요소가 영향을 미칠 수 있기 때문에 실제 데이터베이스와 연결하여 테스트하는 것이 유용합니다.
  • 복잡한 비즈니스 로직 검증 : 데이터베이스와의 통합 테스트는 단순히 데이터를 저장하고 조회하는 것만 검증하지 않습니다. 복잡한 비즈니스 로직이 데이터베이스와 어떻게 상호작용하는지도 중요한 검증 항목입니다.
  • 배포 환경과의 유사성 : 실제 환경에서 데이터베이스와의 상호작용을 테스트함으로써, 배포 후 발생할 수 있는 문제를 미리 파악하고 예방할 수 있습니다.

 

2. 인메모리 데이터베이스 활용

테스트 환경에서 외부 데이터베이스와 연결하는 대신, 인메모리 데이터베이스를 활용하여 더 빠르고 독립적인 테스트하는 방법부터 살펴봅시다. 아래의 설정은 H2 인메모리 데이터베이스를 사용하도록 설정하며, 테스트가 종료되면 데이터베이스가 자동으로 삭제됩니다. 이를 통해 실제 데이터베이스와 연결하지 않고도 효과적으로 테스트를 수행할 수 있어 실제 프로젝트에 사용되는 외부 데이터베이스를 오염시키지 않을 수 있습니다. 스프링 부트는 기본적으로 H2와 같은 인메모리 데이터베이스를 지원하며, 실제 데이터베이스와의 상호작용을 테스트할 수 있게 해줍니다. 아래의 설정 코드는 application-test.properties에 앞선 설명을 실제로 구현한 방법으로, 인메모리 데이터베이스를 활용하고자 한다면 설정을 따라해봅시다.

spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

 

 

3. 스프링 부트에서의 데이터베이스 통합 테스트

이번에는 실제 데이터베이스를 가지고 통합 테스트를 진행하는 방법을 알아봅시다. 스프링 부트는 @SpringBootTest와 @DataJpaTest와 같은 어노테이션을 제공하여 데이터베이스와의 통합 테스트를 쉽게 구현할 수 있습니다.

 

1) @SpringBootTest를 이용한 데이터베이스 통합 테스트

@SpringBootTest는 전체 스프링 컨텍스트를 로드하여 애플리케이션의 여러 구성 요소가 함께 동작하는지 테스트할 수 있는 어노테이션입니다. 이를 사용하면 실제 데이터베이스와의 상호작용을 검증할 수 있습니다. 아래 예제에서는 @SpringBootTest를 사용하여 UserService와 UserRepository를 통합적으로 테스트하고 있습니다. 실제 데이터베이스에서 사용자 정보를 저장하고, 해당 데이터를 검증하는 방식으로 테스트가 이루어집니다.

@SpringBootTest
public class UserServiceTest {

    @Autowired
    private UserRepository userRepository;  // 실제 UserRepository를 주입

    @Autowired
    private UserService userService;  // 실제 UserService를 주입

    @Test
    public void testCreateUser() {
        // Given
        User user = new User("john_doe", "password123");

        // When
        userService.createUser(user);

        // Then
        assertNotNull(userRepository.findByUsername("john_doe"));
    }
}

 

2) @DataJpaTest를 이용한 데이터베이스 테스트

@DataJpaTest는 JPA 레포지토리 관련 테스트를 할 때 사용하는 어노테이션입니다. 이 어노테이션은 JPA 관련 설정만 로드하여 데이터베이스와의 상호작용을 집중적으로 테스트할 수 있게 해줍니다. 아래의 예제에서는 @DataJpaTest를 사용하여 UserRepository의 save 및 find 메서드를 검증합니다. @DataJpaTest는 데이터베이스와의 실제 상호작용을 테스트하여, 데이터를 저장하고 조회하는 작업이 정상적으로 이루어지는지 확인합니다.

@DataJpaTest
public class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    public void testSaveAndFindUser() {
        // Given
        User user = new User("alice", "securepassword");

        // When
        userRepository.save(user);

        // Then
        User foundUser = userRepository.findByUsername("alice");
        assertNotNull(foundUser);
        assertEquals("alice", foundUser.getUsername());
    }
}

 

 

4. 데이터베이스 통합 테스트 시 유의사항

  • 성능: 데이터베이스와의 통합 테스트는 상대적으로 시간이 오래 걸릴 수 있습니다. 테스트 성능을 높이기 위해 인메모리 데이터베이스나 Mocking 기법을 활용할 수 있습니다.
  • 트랜잭션 관리 : 데이터베이스와의 통합 테스트에서는 실제로 데이터를 저장하고 조회하기 때문에, 테스트 후 데이터를 정리하는 것이 중요합니다. 스프링은 @Transactional 어노테이션을 통해 테스트 후 자동으로 트랜잭션을 롤백할 수 있도록 지원하여, 데이터베이스 상태가 초기화된 상태로 유지됩니다.
@Transactional
public void testCreateUser() {
    // Given
    User user = new User("john_doe", "password123");

    // When
    userService.createUser(user);

    // Then
    assertNotNull(userRepository.findByUsername("john_doe"));
}

 

 

5. 데이터베이스와 통합 테스트의 결론

데이터베이스와의 통합 테스트는 애플리케이션의 비즈니스 로직이 실제 데이터베이스와 어떻게 상호작용하는지 검증하는 중요한 과정입니다. @SpringBootTest와 @DataJpaTest와 같은 어노테이션을 사용하면 쉽게 통합 테스트를 구현할 수 있으며, 인메모리 데이터베이스를 활용하면 테스트 환경을 빠르고 효율적으로 설정할 수 있습니다.

 

통합 테스트를 통해 실제 데이터베이스와의 상호작용을 검증하면, 애플리케이션의 품질을 높이고 실제 배포 환경에서 발생할 수 있는 문제를 미리 예방할 수 있습니다. 데이터베이스와의 통합 테스트는 애플리케이션이 의도대로 작동하는지 확인하는 중요한 단계이므로 잘 학습해둡시다.


테스트 실전편 세 번째 : REST API 테스트와 MockMvc

MockMvc에 대한 내용은 실습편 첫 번째에서도 이야기하긴 했지만, 이는 REST API 테스트와 이를 쉽게 구현할 수 있게 도와주기도 하기 때문에 다시 한번 MockMvc에 대해 다뤄보겠습니다.

 

1. REST API 테스트란?

REST API는 현대 웹 애플리케이션에서 중요한 역할을 하며, 특히 스프링 부트 애플리케이션에서는 RESTful 서비스를 구현하는 것이 일반적입니다. 따라서, 이러한 API가 의도한 대로 동작하는지 검증하는 테스트는 매우 중요합니다. REST API 테스트는 RESTful 웹 서비스를 테스트하는 과정으로, HTTP 요청(GET, POST, PUT, DELETE 등)을 보내고, 응답을 검증하는 방식입니다. 이를 통해 개발자는 API가 올바르게 동작하는지, 예외 처리나 경계 조건에서도 제대로 작동하는지를 확인할 수 있습니다.

 

 

2. API 테스트의 주요 목적

  • 정상 동작 검증 : API가 올바르게 동작하는지 확인합니다. 예를 들어, 특정 URL로 GET 요청을 보냈을 때, 예상한 데이터가 제대로 반환되는지 체크합니다.
  • 에러 처리 검증 : API가 잘못된 요청에 대해 적절한 에러 메시지와 상태 코드를 반환하는지 확인합니다.
  • 성능 및 부하 검증 : 대량의 데이터나 동시 요청을 처리할 때 API의 성능을 테스트할 수 있습니다.
  • 보안 검증 : 인증 및 권한 처리 로직이 올바르게 작동하는지 검증합니다.

 

3. MockMvc의 개념

MockMvc는 스프링 테스트 모듈의 일부로, HTTP 요청을 시뮬레이션하여 실제 서버를 구동하지 않고도 웹 계층을 테스트할 수 있는 도구입니다. MockMvc를 사용하면 실제 HTTP 요청을 보내고 응답을 검증할 수 있기 때문에, REST API의 동작을 효율적으로 테스트할 수 있습니다. MockMvc를 사용하면 다음과 같은 테스트를 수행할 수 있습니다:

  • HTTP 요청 : GET, POST, PUT, DELETE 등의 요청을 시뮬레이션합니다.
  • 상태 코드 검증 : API 응답의 HTTP 상태 코드(200 OK, 404 Not Found 등)를 검증할 수 있습니다.
  • 응답 본문 검증 : JSON 형태로 반환되는 응답 본문을 검증할 수 있습니다.
  • 헤더 검증 : 응답 헤더에 포함된 정보를 검증할 수 있습니다.
  • 예외 처리 검증 : 예상치 못한 오류 발생 시 적절한 에러 메시지와 상태 코드가 반환되는지 확인할 수 있습니다.

 

4. MockMvc 설정

MockMvc를 사용하기 위해서는 먼저 테스트 클래스에 @WebMvcTest 또는 @SpringBootTest 어노테이션을 활용하여 웹 환경을 설정해야 합니다. @WebMvcTest는 컨트롤러를 테스트할 때 유용하며, @SpringBootTest는 전체 애플리케이션을 테스트할 때 사용합니다.

 

1) @WebMvcTest 예시

@WebMvcTest는 주로 웹 계층 테스트를 위해 사용되며, 컨트롤러와 그 관련 빈만 로드합니다. 이 어노테이션은 테스트 속도를 높이는 데 유리합니다. 아래 코드에서 @WebMvcTest는 ItemController만 테스트하고, MockBean을 사용해 ItemService를 Mock 객체로 주입합니다. mockMvc.perform(get("/items"))를 통해 GET 요청을 시뮬레이션하고, 응답을 검증하는 방식입니다.

@RunWith(SpringRunner.class)
@WebMvcTest(ItemController.class)  // 테스트 대상인 컨트롤러를 지정
public class ItemControllerTest {

    @Autowired
    private MockMvc mockMvc;  // MockMvc 주입

    @MockBean
    private ItemService itemService;  // Mock된 서비스 주입

    @Test
    public void testGetItems() throws Exception {
        // Given
        List<Item> items = Arrays.asList(
            new Item(1L, "Item1", 100),
            new Item(2L, "Item2", 200)
        );
        when(itemService.getItems()).thenReturn(items);

        // When & Then
        mockMvc.perform(get("/items"))  // GET 요청
               .andExpect(status().isOk())  // 상태 코드 200 확인
               .andExpect(jsonPath("$.length()").value(2))  // JSON 응답의 길이 검증
               .andExpect(jsonPath("$[0].name").value("Item1"));  // 첫 번째 아이템의 이름 검증
    }
}

 

2) @SpringBootTest 예시

전체 애플리케이션을 테스트할 때는 @SpringBootTest를 사용하여 더 넓은 범위에서 테스트를 할 수 있습니다. @SpringBootTest와 @AutoConfigureMockMvc를 사용하여 전체 애플리케이션을 통합적으로 테스트하고, 실제 HTTP 요청을 보내는 방식입니다. post("/items")로 POST 요청을 보내고, 응답 본문과 상태 코드를 검증합니다.

@SpringBootTest
@AutoConfigureMockMvc
public class ItemControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testCreateItem() throws Exception {
        // Given
        String itemJson = "{\"name\":\"New Item\",\"price\":150}";

        // When & Then
        mockMvc.perform(post("/items")
                .contentType(MediaType.APPLICATION_JSON)
                .content(itemJson))  // POST 요청과 본문 데이터 설정
               .andExpect(status().isCreated())  // 상태 코드 201 확인
               .andExpect(jsonPath("$.name").value("New Item"))  // 응답 JSON의 'name' 필드 검증
               .andExpect(jsonPath("$.price").value(150));  // 응답 JSON의 'price' 필드 검증
    }
}

 

 

5. MockMvc 활용한 REST API 테스트의 장점

  • 빠른 테스트 : 실제 서버를 실행하지 않고, HTTP 요청을 시뮬레이션할 수 있어 테스트 속도가 빠릅니다.
  • 독립적인 테스트 : 데이터베이스나 외부 API와의 연결 없이 컨트롤러 단위만 독립적으로 테스트할 수 있습니다.
  • 응답 검증 : HTTP 상태 코드, 응답 본문, 헤더 등 다양한 측면을 검증할 수 있어 API의 동작을 철저히 확인할 수 있습니다.
  • Mocking 활용 : 실제 서비스나 레포지토리와의 상호작용을 Mocking하여 테스트할 수 있어, 데이터베이스에 영향을 주지 않으며 빠르게 테스트할 수 있습니다.

 

6. MockMvc로 REST API 테스트 시 고려할 사항

  • 테스트 범위 : MockMvc는 웹 계층의 테스트에 집중되므로, 서비스 계층이나 데이터베이스 연동이 필요한 경우 @MockBean을 사용하여 필요한 의존성을 Mocking할 수 있습니다.
  • 에러 처리 : API의 예외 처리 로직도 중요하므로, 다양한 예외 상황에 대한 테스트도 함께 진행해야 합니다. 예를 들어, 잘못된 요청이나 인증 실패 등의 상황을 검증할 필요가 있습니다.
  • 응답 본문 검증 : JSON 응답의 구조를 정확하게 확인하는 것이 중요합니다. jsonPath()를 사용하여 응답 데이터를 구체적으로 검증할 수 있습니다.

 

7. REST API 테스트와 MockMvc의 결론

REST API의 동작을 검증하는 테스트는 애플리케이션의 품질을 보장하는 중요한 과정입니다. MockMvc를 활용하면, 실제 서버를 실행하지 않고도 HTTP 요청을 시뮬레이션하여 빠르고 효율적으로 API의 동작을 테스트할 수 있습니다. @WebMvcTest와 @SpringBootTest를 적절하게 활용하여, 컨트롤러 단위부터 전체 애플리케이션까지 다양한 테스트를 수행할 수 있습니다. 


테스트 실전편 네 번째 : 모의 객체(Mock Objects)와 의존성 주입

도메인 엔티티는 그 자체로 단순한 데이터 객체이며, 내부에 복잡한 로직이나 외부 의존성이 없기 때문에 굳이 목(mock) 객체로 생성할 필요가 없습니다.


테스트 실전편 다섯 번째 : 예외 처리 및 경계 조건 테스트

 


 

테스트 실전편 여섯 번째 : 성능 테스트 및 최적화

 


 

테스트 실전편 일곱 번째 : CI/CD와 통합된 테스트 파이프라인 구성

 


 

테스트 실전편 여덟 번째 : 테스트 코드의 유지보수성


 

테스트 번외편 : 테스트할 때 유의해야 할 점

테스트를 작성할 때 유의해야 할 점들은 잘 알아두는 것은 테스트의 정확성, 효율성, 유지보수성을 높이고, 불필요한 오류나 리소스를 낭비하지 않도록 돕습니다. 여기서는 단위 테스트(Unit Test)통합 테스트(Integration Test)Mocking 각각에서 중요한 고려사항은 무엇인지 살펴봅시다.

 

 

1. 테스트의 목적과 범위 명확히 하기

테스트를 작성하기 전에 해당 테스트가 어떤 목적을 가지고 있는지 명확하게 설정하는 것이 중요합니다. 테스트의 범위가 너무 넓거나 모호하면, 유지보수에 어려움을 겪을 수 있습니다. 

  • 단위 테스트는 작은 단위의 코드만 테스트하고, 의존성은 가능한 Mocking 처리하여 독립적인 테스트를 수행해야 합니다.
  • 통합 테스트는 여러 컴포넌트가 잘 상호작용하는지 확인하며, 실제 시스템과 가까운 환경에서 동작을 검증합니다.

정리하면, 단위 테스트와 통합 테스트의 개념을 명확히 가지고 범위를 잘 잡아야 합니다.

 

 

2. 테스트 코드의 가독성 유지

테스트 코드도 코드입니다. 이해하기 어려운 테스트 코드는 추후 유지보수 시 혼란을 일으킬 수 있습니다. 테스트 코드의 가독성을 높이기 위한 몇 가지 팁은 다음과 같습니다.

  • 명확한 이름 사용 : 테스트 메서드나 클래스 이름에 어떤 기능을 테스트하는지 명확하게 나타내세요. 예: testCreateOrder(), testServiceMethod_withValidInput_returnsCorrectResult()
  • Arrange-Act-Assert 패턴 사용 : 테스트의 흐름을 준비(Arrange) → 실행(Act) → 검증(Assert) 순으로 구분하여 코드를 작성하는 방법을 의미합니다. 이를 잘 기억해두면 앞으로 보여질 테스트 코드 예시의 흐름이 이를 따른다는 사실이 보일 겁니다.
@Test
void testServiceMethod() {

    // Arrange 테스트 준비
    var service = new MyService();
    
    // Act 테스트 수행
    var result = service.method();
    
    // Assert 테스트 결과 검증
    assertEquals(expected, result);
}

 

 

3. 테스트 코드의 중복 피하기

앞서 말했듯이 테스트 코드도 코드입니다. 테스트 코드는 가능한 한 중복을 피하고 재사용성을 높여야 합니다. 유틸리티 메서드기본 데이터 세팅 등을 별도로 분리하여 관리하면 코드의 중복을 줄일 수 있습니다. 

  • @BeforeEach : 매 테스트 전에 공통으로 실행해야 할 코드가 있다면 @BeforeEach를 사용하여 중복을 제거할 수 있습니다.
  • Test Utility Class : 반복적으로 사용되는 코드나 데이터 준비 로직은 별도의 유틸리티 클래스로 분리하여 관리할 수 있습니다.

 

4. 테스트 독립성 확보

테스트는 독립적이어야 합니다. 하나의 테스트가 다른 테스트의 결과에 영향을 미치지 않아야 하며, 각 테스트는 독립적으로 실행될 수 있어야 합니다. 이는 매우 중요한 사항으로 시스템이 복잡해졌을 때나 데이터베이스와 의존되었을 때 큰 문제가 발생하므로 잘 기억해둡시다.

  • 상태 공유 방지 : 한 테스트에서 변경한 데이터나 상태가 다른 테스트에 영향을 주지 않도록 해야 합니다. 특히, 데이터베이스나 파일 시스템을 사용하는 경우, 테스트가 끝날 때마다 상태를 초기화하거나 Mocking을 통해 외부 의존성을 분리하는 것이 좋습니다.
  • Test Cleanup : @After나 @AfterEach를 활용하여 테스트 후 데이터를 정리하거나 상태를 리셋하는 방법을 사용할 수 있습니다.

 

5. 테스트 실행 순서와 의존성 관리

테스트는 독립적이어야 하므로, 실행 순서에 의존하지 않도록 작성해야 합니다. 실행 순서가 중요한 테스트를 작성하게 되면, 코드 변경에 의해 테스트의 순서가 변경될 때 오류가 발생할 수 있습니다.

  • 의존성 없는 테스트 작성 : 하나의 테스트가 다른 테스트의 실행 결과에 의존하지 않도록 해야 합니다.
  • 테스트 그룹화 : 비슷한 성격의 테스트는 그룹화하여 실행할 수 있지만, 각 테스트가 독립적으로 실행될 수 있도록 해야 합니다. 쉽게 말하면 테스트 실행 버튼 한개 누를 때, 다른 테스트에 영향이 가면 어떻게 할까 등의 고민이 없게 테스트 코드를 작성해야 한다는 이야기입니다.

 

6. 외부 의존성 최소화

외부 시스템(DB, 외부 API 등)에 의존하는 테스트는 실행 속도가 느려질 수 있습니다. 가능하면 Mocking을 활용하여 외부 시스템을 대체하거나, 실제 시스템과 연동되는 통합 테스트만 진행하는 것이 좋습니다.

  • Embedded DB 사용 : 테스트에서 데이터베이스를 사용할 경우, 실제 DB 대신에 H2나 HSQLDB와 같은 내장형 데이터베이스를 사용하는 것이 테스트 속도에 도움이 됩니다. 위의 실전편에서는 H2를 사용하였습니다.
  • 외부 API Mocking : 외부 API와 통신하는 부분은 WireMock이나 Mockito를 사용하여 Mocking 처리하는 것이 좋습니다.

 

7. 빠르고 효율적인 테스트 작성

테스트는 빠르고 효율적으로 실행되어야 합니다. 너무 많은 데이터를 처리하거나 복잡한 로직을 테스트에 포함시키면 테스트가 느려질 수 있습니다. 실제 서비스와 동일한 환경을 테스트할 때는 가능하면 Mocking을 사용하여 테스트 속도를 높이세요.

  • 단위 테스트에서의 효율성: 단위 테스트는 가급적 빠르게 실행되어야 하므로, 외부 시스템을 Mock 처리하거나, 작은 단위의 데이터로 테스트를 진행하는 것이 좋습니다.
  • 통합 테스트에서의 리소스 사용: 통합 테스트에서는 실제 DB나 외부 시스템과 연결할 수 있지만, 이때 불필요한 데이터나 대용량의 데이터를 사용하지 않도록 주의해야 합니다.

 

8. Mocking을 올바르게 활용

실전 편에서 Mock에 대해 코드와 함께 학습할 예정인데, 이를 알고나면 눈이 번쩍 뜨이면서 매번 사용하려고 들지도 모릅니다. 그러나, Mocking은 테스트의 효율성을 높이는 데 매우 유용하지만, 과도하게 사용하면 실제 코드의 동작을 정확히 반영하지 못할 수 있습니다. Mocking은 필요할 때만 사용하고, 상황에 따라 실제 컴포넌트나 외부 시스템과 상호작용하는 테스트도 포함시킬 줄도 알아야 합니다.

  • Mocking 의존성 : 서비스나 레포지토리 등의 의존성을 Mock 객체로 대체할 때, 해당 객체가 제대로 동작하도록 설정해야 합니다. 잘못된 Mock 설정은 테스트의 신뢰성을 떨어뜨릴 수 있습니다.
  • 기능 검증 : 중요한 비즈니스 로직이나 외부 API와의 통합이 중요한 경우, Mocking으로 대체하기보다는 실제 환경에서 테스트를 진행하는 것이 좋습니다.

 

9. 테스트 실패 시 디버깅

테스트가 실패할 경우, 왜 실패했는지 정확히 알 수 있도록 명확한 에러 메시지와 로그를 남기는 것이 중요합니다.

  • 명확한 에러 메시지 : 실패한 테스트에서 발생한 예외나 오류에 대한 정보를 충분히 제공하여 디버깅을 용이하게 해야 합니다.
  • 로깅 : @Test 메서드 내부에서 로그를 출력하거나, 특정 로직을 확인할 수 있도록 assertThat() 등을 활용한 검증을 추가하여 실패 이유를 파악할 수 있게 합니다. 

 

10. 지속적인 테스트와 코드 커버리지 분석

테스트는 한 번 작성하고 끝나는 것이 아니라, 지속적으로 실행하여 결과를 검토하고 개선하는 것이 중요합니다. 테스트 코드의 커버리지는 중요한 지표입니다. 테스트가 전체 코드의 얼마나 많은 부분을 검사하는지 확인할 수 있습니다. 커버리지 비율을 높이기 위해서는 각 메서드와 클래스가 정상적으로 동작하는지 확인할 수 있는 테스트를 작성해야 합니다. 리팩토링 시에도 테스트 코드가 필요합니다. 리팩토링을 할 때 테스트가 없다면, 기존 기능이 여전히 정상적으로 동작하는지 확인할 수 없습니다.

  • JUnit5와 커버리지 도구 : JaCoCo와 같은 코드 커버리지 도구를 사용하여 테스트 코드가 전체 애플리케이션 코드를 충분히 커버하는지 확인할 수 있습니다.

 

728x90