본문 바로가기

개념정리(Spring)

Spring Test

 혼자서 작업할 땐 몰랐지만 팀으로 프로젝트를 경험하다보니 느낀점이 있다. 아무리 역할을 나누더라도 서로 필요한 부분이 있고 이 때, 소통이 원할하지 않으면 큰일이 날 수 있다는 사실, 그리고 다른 팀원이 코드를 바꿨을 때, 내 로직에 영향이 간다면 왜 오류가 생기는지 찾는데 정말 많은 시간을 소모할 것이다. 또한 나의 작업 진행도와 완성된 결과물이 어떻게 동작하는지 보여주기 위한 케이스가 필요하다는 것을 느꼈다.

 

 이런 부분을 생각하지 않고 개발한 코드를 레거시 코드라고 하는데, 주석이 없어 파악이 어렵고, 테스트 코드가 없는 문제 등으로 개발자가 건들기 무서워하는 코드가 되어버린다고 한다. 이렇게 개발하고 싶지 않다면 내 코드가 어떠한 상황에서 동작이 잘 되는지, 어떤식으로 동작하는지 보여주기 위한 테스트 코드가 필요해진다.

 

🔍  소프트웨어 테스트란?

소프트웨어의 기능과 동작, 데이터 등이 의도한대로 처리되는지 확인하는 과정이다. 또한 그 속에서 결함을 찾아내고 수정하는 과정이라고 할 수 있다. 이를 위해 독립적으로 격리된 환경(테스트 환경)을 만들고 특정한 조건(상황)을 세팅하고(시나리오), 코드를 실행하여 결과값이 내가 의도한 값과 같은지 검증한다.

 

📁  테스트 코드: 테스트를 반복할 수 있는 틀

위에서 설명한 소프트웨어 테스트를 코드로 만든 것이 테스트 코드이다. 이는 테스트를 반복적으로 실행 가능해야하며, 의도한 시나리오가 비지니스 흐름과 로직으로 담겨져 있어서 문서로서의 기능을 수행할 수 있다. 

 

📚  테스트 코드 작성 패턴

 테스트 코드에는 Given - When - Then 패턴으로 이루어진 작성 패턴이 존재한다.

1. Given : 시나리오

 테스트를 준비하는 과정, 테스트에 필요한 변수, 입력 값을 준비하는 단계이다.2. When : 실행 실제로 테스트를 실행해보는 단계이다.3. Then : 검증 마지막으로 테스트를 검증하는 단계로 예상한 값과 결과 값을 비교하여 검증한다.

 

🧪 테스트 코드 개발 도구

 스프링에서는 테스트를 위한 어노테이션을 제공하는데

 

🧪 @Test

  • Junit5에서 테스트 메서드임을 나타냄
  • 해당 메서드가 테스트 메서드로 실행됨.
@Test 
void testAdd() { 
	assertEquals(4, calculator.add(2, 2)); 
}

📌 @DisplayName("...")

  • 테스트 이름을 지정해줌. 실행 결과에 표시됨.
  • 보기 좋은 테스트 결과를 만들 수 있음.
@Test 
@DisplayName("2 + 2는 4가 되어야 한다") 
void addTest() {
	assertEquals(4, calculator.add(2, 2)); 
}

🔥 @SpringBootTest

  • 스프링 전체 컨텍스트를 로드해서 통합 테스트를 수행할 때 사용
  • 실제 서버처럼 애플리케이션을 띄워서 테스트 가능 (DB, 서비스, 레포지토리 다 사용 가능)
  • 무겁지만 실제 운영 환경과 유사하게 테스트할 수 있음.
@SpringBootTest 
class UserServiceIntegrationTest { 
    @Autowired UserService userService; 
    
    @Test 
    void createUser() { 
        // 실제 DB에 insert, 서비스 호출 등 가능 
    } 
}

💻 @WebMvcTest

  • 컨트롤러 계층만 테스트할 때 사용
  • @Service, @Repository 등은 자동으로 로드되지 않음 → 필요한 경우 @MockBean 사용
@WebMvcTest(UserController.class) 
class UserControllerTest { 
    @Autowired MockMvc mockMvc; 
    @MockBean UserService userService; 
    
    @Test 
    void testGetUser() throws Exception { 
    	when(userService.getUser(1L)).thenReturn(new User(...)); 
        mockMvc.perform(get("/users/1")).andExpect(status().isOk()); 
    } 
}

💽 @DataJpaTest

  • JPA 관련 컴포넌트들만 로드하여 테스트함 (@Entity, @Repository)
  • 내장 DB(H2 등) 사용해서 빠르게 테스트 가능
@DataJpaTest 
class UserRepositoryTest { 
    @Autowired UserRepository userRepository; 
    
    @Test 
    void testFindByUsername() { 
        userRepository.save(new User("minseo")); 
        Optional<User> found = userRepository.findByUsername("minseo"); 
        assertTrue(found.isPresent()); 
    } 
}

🧰 기타 유용한 애너테이션

🔧 @BeforeEach

  • 테스트 메서드 실행 전에 공통적으로 실행할 코드 정의
  • 테스트마다 반복되는 초기 설정
@BeforeEach 
void setup() {
    user = new User("minseo"); 
}

🧹 @AfterEach

  • 테스트 메서드 실행 후 정리 작업을 할 때 사용
  • DB 초기화, 리소스 정리 등
@AfterEach 
void cleanup() { 
    userRepository.deleteAll(); 
}

🌊 @MockBean

  • 테스트 환경에서 특정 빈을 mock 객체로 대체
  • 주로 @WebMvcTest 또는 @SpringBootTest에서 의존성 주입받는 빈을 가짜로 대체할 때 사용
@MockBean 
private UserService userService; 

@Test 
void testMocking() { 
    when(userService.findUser(any())).thenReturn(new User("mock")); 
}

mock 객체는 다음에 자세히 알아볼 예정이다.

 


🚀  테스트 코드 AssertJ 사용법

기본 타입 검증

assertThat(1 + 1).isEqualTo(2);                 // 값이 2인지 검증
assertThat(10).isGreaterThan(5);               // 10 > 5
assertThat(3.14).isCloseTo(3.1, within(0.1));   // 오차 범위 내의 값인지
assertThat(true).isTrue();                     // true 여부 검증
assertThat("hello").isNotEmpty();              // 문자열이 비어있지 않은지

 

컬렉션 / 리스트

List<String> names = List.of("민서", "철수", "영희");

assertThat(names).hasSize(3);                         // 크기 검증
assertThat(names).contains("민서", "철수");            // 포함된 값 확인
assertThat(names).doesNotContain("기웅");             // 포함되지 않은 값
assertThat(names).containsExactly("민서", "철수", "영희");  // 순서까지 정확히 일치
assertThat(names).startsWith("민서");                  // 시작 요소
assertThat(names).endsWith("영희");                   // 끝 요소

 

예외 발생 검증(try-catch 없이)

// 예외가 발생하는 메서드
ThrowingCallable throwing = () -> {
    throw new IllegalArgumentException("잘못된 인자입니다");
};

assertThatThrownBy(throwing)
    .isInstanceOf(IllegalArgumentException.class)
    .hasMessageContaining("잘못된");
    
// 또는
assertThatExceptionOfType(IllegalArgumentException.class)
    .isThrownBy(() -> someService.doSomethingWrong())
    .withMessageContaining("잘못된");

 

map 검증

Map<String, Integer> scores = Map.of(
    "민서", 90,
    "철수", 85
);

assertThat(scores).containsKey("민서");
assertThat(scores).containsEntry("철수", 85);
assertThat(scores).doesNotContainKey("기웅");
assertThat(scores.keySet()).hasSize(2);

 

사용법을 먼저 알아봤는데 이처럼 AssertJ는 자바 Junit 테스트 코드의 가독성과 편의성을 높여주는 라이브러리이다.


✅  테스트 코드 만들기

이제 지금까지 알아본 것들을 활용하여 테스트 코드를 구성해보자. UserService는 사용자를 조회하는 서비스이고, UserRepository는 사용자의 데이터 레포지토리 (Jpa) 일 때, 

 

@SpringBootTest
@DisplayName("UserService 통합 테스트")
class UserServiceTest {

    @MockBean
    private UserRepository userRepository;

    @Autowired
    private UserService userService;

    private User testUser;

    @BeforeEach
    void setUp() {
        testUser = new User(1L, "minseo", "minseo@example.com");
        when(userRepository.findById(1L)).thenReturn(Optional.of(testUser));
    }

    @AfterEach
    void tearDown() {
        // 필요한 정리 작업
        System.out.println("테스트 종료");
    }

    @Test
    @DisplayName("유저 ID로 사용자 조회 성공")
    void findUserById_success() {
        User user = userService.findUserById(1L);

        assertThat(user).isNotNull();
        assertThat(user.getUsername()).isEqualTo("minseo");
        assertThat(user.getEmail()).contains("@example.com");
    }

    @Test
    @DisplayName("없는 유저 ID 조회 시 예외 발생")
    void findUserById_notFound() {
        when(userRepository.findById(2L)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> userService.findUserById(2L))
            .isInstanceOf(UserNotFoundException.class)
            .hasMessageContaining("User not found");
    }
}

 전체적으로 이런 식으로 테스트 코드를 구성할 수 있다. 먼저 이 테스트는 UserService 클래스의 메서드인 findUserById()가 정상 작동하는지, 예외 처리는 잘 되는지를 검증하는 통합 테스트이다.

📋  테스트 구성

@BeforeEach

각 테스트가 실행되기 전에 공통적으로 사용할 데이터를 정의한다.
이 예제에서는 UserRepository가 1L이라는 ID를 조회하면 항상 testUser 객체를 반환하도록 설정했다.

@AfterEach

테스트가 끝날 때마다 실행되며, 테스트 종료를 나타내는 로그를 출력하였고, 리소스를 정리하거나 테스트 후 처리가 필요할 때 유용하다.

 

💻  테스트 시나리오

✅ findUserById_success

  • 1L ID를 가진 유저가 존재할 경우
  • User 객체를 정상적으로 반환함
  • 성공한 테스트 케이스

❌ findUserById_notFound

  • 2L ID를 가진 유저가 존재하지 않을 경우
  • UserNotFoundException 예외가 발생
  • 실패(예외 발생) 테스트 케이스

이처럼 ID에 따라 테스트 상황을 나누고, 결과가 기대와 일치하는지를 검증했다.

 

💭 마치며

테스트를 작성하면서 느낀 점은, 단순히 코드를 작성하는 것에서 그치지 않고, 다양한 상황을 미리 생각해볼 수 있다는 점이 매우 좋았다.

  • 어떤 상황에서 예외가 발생할 수 있는지
  • 특정 입력이 들어왔을 때 내가 만든 서비스가 어떻게 반응하는지

테스트 케이스를 구성하는 것만으로도 여러 시나리오를 미리 시뮬레이션해볼 수 있고, 그에 따라 에러에 대한 대응력을 높일 수 있다는 장점이 있다.

 

개발 시간이 허락된다면, 이런 테스트는 선택이 아닌 필수로 함께 작성하여 내가 만든 로직이 의도한 대로 작동하는지 검증하고, 더 견고하고 신뢰할 수 있는 코드를 만들고자 한다. 이러한 작은 습관 하나하나가 상황에 빠르게 대응할 수 있는 좋은 개발자로 성장하는 밑거름이 될 거라고 생각한다. 😊

'개념정리(Spring)' 카테고리의 다른 글

비동기 작업과 Thread Pool  (0) 2025.04.02