Test Code

2025-06-13

테스트는 요구사항에 따라 정확하게 동작하는 지 확인하는 과정이다.

테스트의 필요성

좋은 테스트 코드란

F.I.R.S.T 원칙

Fast: 테스트는 자주 실행되므로 빠르게 동작해야 합니다.

Independent: 각 테스트는 다른 테스트의 실행 결과에 영향을 받지 않아야 합니다.

Repeatable: 어떤 환경에서든, 몇 번을 실행하든 항상 동일한 결과를 내야 합니다.

Self-Validating: 테스트는 성공 또는 실패를 명확하게 알려주어야 합니다.

Timely: 테스트할 코드를 작성하기 직전 또는 작성하면서 함께 작성되어야 합니다.

특성

Unit Test

단위 테스트는 애플리케이션에서 가장 작은 단위의 코드(메소드, 클래스)를 독립적으로 테스트하는 것이다.

Spring Boot 기본 테스트 환경

spring-boot-starter-test

dependencies {
   testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform() // JUnit 5를 사용하도록 설정
}

JUnit 5 테스트 구조


테스트 코드 예시

// src/test/java/com/example/myspringtestproject/ExampleTest.java

package com.example.myspringtestproject;

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

// 단독으로 실행되는 간단한 JUnit 5 테스트 예시
class ExampleTest {

    @Test // 이 메소드가 테스트 메소드임을 나타냅니다.
    @DisplayName("덧셈 연산이 올바르게 동작하는지 테스트") // 테스트의 목적을 명확히 설명합니다.
    void testAddition() {
        // Given (테스트를 위한 준비)
        int a = 5;
        int b = 3;

        // When (테스트 대상 메소드 실행)
        int sum = a + b;

        // Then (결과 검증)
        // AssertJ의 assertThat을 사용하여 가독성 좋게 검증할 수 있습니다.
        // import static org.assertj.core.api.Assertions.assertThat;
        // assertThat(sum).isEqualTo(8);
        Assertions.assertEquals(8, sum, "5 + 3은 8이어야 합니다.");
    }

    @Test
    @DisplayName("문자열 길이를 정확히 반환하는지 테스트")
    void testStringLength() {
        // Given
        String text = "hello";

        // When
        int length = text.length();

        // Then
        Assertions.assertEquals(5, length);
    }
}

Given-When-Then 패턴

class ExampleTest {

    @Test
    @DisplayName("덧셈 연산이 올바르게 동작하는지 테스트")
    void testAddition() {
        // Given (테스트를 위한 준비)
        int a = 5;
        int b = 3;

        // When (테스트 대상 메소드 실행)
        int sum = a + b;

        // Then (결과 검증)
        assertThat(sum).isEqualTo(8);
    }
    
}

실제 Spring Boot 애플리케이션 테스트 예제


// src/test/java/com/example/myspringtestproject/user/UserServiceTest.java
package com.example.myspringtestproject.user;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
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 java.util.Arrays;
import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class) // Mockito를 JUnit 5와 함께 사용하기 위한 확장
class UserServiceTest {

    @Mock // 이 필드가 Mock 객체임을 나타냅니다.
    private UserRepository userRepository;

    @InjectMocks // Mock 객체들이 주입될 실제 객체(테스트 대상)
    private UserService userService;

    // 각 테스트 메소드 실행 전에 초기화 로직이 필요하다면 사용
    @BeforeEach
    void setUp() {
        // MockitoAnnotations.openMocks(this); // @ExtendWith(MockitoExtension.class)를 사용하면 필요 없음
    }

    @Test
    @DisplayName("새로운 사용자를 생성한다.")
    void createUser_ValidUser_ReturnsNewUser() {
        // Given
        User newUser = new User(null, "John Doe", "[email protected]");
        User savedUser = new User(1L, "John Doe", "[email protected]");

        // Mocking: userRepository.save(any(User.class))가 호출되면 savedUser를 반환하도록 설정
        // any(User.class): 어떤 User 객체가 넘어와도 이 Stubbing이 동작하도록 합니다.
        when(userRepository.findByEmail(newUser.getEmail())).thenReturn(Optional.empty()); // 이메일 중복 없음
        when(userRepository.save(any(User.class))).thenReturn(savedUser);

        // When
        User result = userService.createUser(newUser);

        // Then
        assertThat(result).isNotNull();
        assertThat(result.getId()).isEqualTo(1L);
        assertThat(result.getName()).isEqualTo("John Doe");
        assertThat(result.getEmail()).isEqualTo("[email protected]");

        // Mocking: save 메소드가 정확히 한 번 호출되었는지 검증
        verify(userRepository, times(1)).save(any(User.class));
        verify(userRepository, times(1)).findByEmail(newUser.getEmail());
    }

    @Test
    @DisplayName("이미 존재하는 이메일로 사용자 생성 시 예외가 발생한다.")
    void createUser_ExistingEmail_ThrowsException() {
        // Given
        User existingUser = new User(1L, "Jane Doe", "[email protected]");
        User newUser = new User(null, "John Doe", "[email protected]"); // 이미 존재하는 이메일

        // Mocking: findByEmail 호출 시 Optional.of(existingUser) 반환하도록 설정
        when(userRepository.findByEmail(newUser.getEmail())).thenReturn(Optional.of(existingUser));

        // When & Then
        // assertThatThrownBy를 사용하여 특정 예외가 발생하는지 검증
        assertThatThrownBy(() -> userService.createUser(newUser))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("이미 사용 중인 이메일입니다.");

        // save 메소드는 호출되지 않았음을 검증
        verify(userRepository, never()).save(any(User.class));
        verify(userRepository, times(1)).findByEmail(newUser.getEmail());
    }

    @Test
    @DisplayName("ID로 사용자를 조회한다.")
    void getUserById_ExistingId_ReturnsUser() {
        // Given
        Long userId = 1L;
        User user = new User(userId, "Test User", "[email protected]");

        // Mocking: findById 호출 시 Optional.of(user) 반환하도록 설정
        when(userRepository.findById(userId)).thenReturn(Optional.of(user));

        // When
        Optional<User> result = userService.getUserById(userId);

        // Then
        assertThat(result).isPresent(); // Optional이 값을 가지고 있는지 확인
        assertThat(result.get().getName()).isEqualTo("Test User");

        // Mocking: findById 메소드가 정확히 한 번 호출되었는지 검증
        verify(userRepository, times(1)).findById(userId);
    }

    @Test
    @DisplayName("존재하지 않는 ID로 사용자 조회 시 Optional.empty()를 반환한다.")
    void getUserById_NonExistingId_ReturnsEmptyOptional() {
        // Given
        Long userId = 99L;

        // Mocking: findById 호출 시 Optional.empty() 반환하도록 설정
        when(userRepository.findById(userId)).thenReturn(Optional.empty());

        // When
        Optional<User> result = userService.getUserById(userId);

        // Then
        assertThat(result).isEmpty(); // Optional이 비어 있는지 확인

        verify(userRepository, times(1)).findById(userId);
    }

    @Test
    @DisplayName("모든 사용자를 조회한다.")
    void getAllUsers_ReturnsListOfUsers() {
        // Given
        List<User> users = Arrays.asList(
                new User(1L, "User1", "[email protected]"),
                new User(2L, "User2", "[email protected]")
        );

        // Mocking: findAll 호출 시 users 리스트 반환하도록 설정
        when(userRepository.findAll()).thenReturn(users);

        // When
        List<User> result = userService.getAllUsers();

        // Then
        assertThat(result).hasSize(2);
        assertThat(result.get(0).getName()).isEqualTo("User1");

        verify(userRepository, times(1)).findAll();
    }
}