Spring에서 테스트 코드 잘 작성하기

Lord·2024년 11월 14일

Knowledge

목록 보기
2/4
post-thumbnail

Spring 애플리케이션에서 테스트 코드를 작성하는 것은 코드의 품질을 보장하고, 기능이 의도한 대로 동작하는지를 확인하는 매우 중요한 작업이다. 이 글에서는 내가 공부했던 내용을 정리하기 위해 작성을 시작하였고 실제로 프로젝트에서 내가 작성한 테스트 코드와 함께, 좋은 테스트 코드의 요건, 그리고 테스트 커버리지에 대해 설명해보려고 한다.

Spring 테스트 라이브러리 소개

Spring에서 테스트 코드를 작성할 때 사용할 수 있는 주요 라이브러리는 다음과 같다.

  1. JUnit: 자바 애플리케이션 테스트를 위한 가장 널리 사용되는 테스트 프레임워크이다. Spring에서는 JUnit을 기본으로 다양한 테스트를 작성할 수 있다. 현재는 JUnit 5(Jupiter)를 많이 사용하며, 유닛 테스트(Unit Test)를 통해 작은 단위의 코드를 독립적으로 테스트할 수 있도록 도와준다.
  2. Spring Test & Spring Boot Test: Spring은 자체적으로 테스트를 지원하는 여러 유틸리티를 제공한다. @SpringBootTest는 애플리케이션 전체를 로드하여 통합 테스트를 수행할 수 있도록 해준다. 또한, @WebMvcTest, @DataJpaTest 등의 어노테이션을 통해 특정 계층만을 테스트할 수도 있다. 이러한 어노테이션은 테스트의 범위를 제한하여 테스트 속도를 개선하고, 필요한 환경만 설정하도록 도와준다.
  3. Mockito: 의존성을 가진 객체를 모의(Mock)하기 위해 사용하는 라이브러리이다. 서비스 계층의 의존성을 줄이고 독립적으로 테스트하기 위해 자주 사용된다. Mockito를 사용하면 데이터베이스나 외부 시스템과의 의존성을 줄여, 순수 비즈니스 로직을 독립적으로 검증할 수 있다.
  4. MockMvc: Spring MVC 컨트롤러를 테스트하기 위해 사용된다. 실제 서버를 띄우지 않고도 HTTP 요청 및 응답을 테스트할 수 있게 해준다. 이 도구는 REST API 테스트에 특히 유용하며, 엔드포인트의 입력, 출력 및 오류 처리를 검증할 수 있도록 돕는다.

이러한 라이브러리들을 사용하면 다양한 상황을 효율적으로 테스트할 수 있으며, 각 계층의 로직을 독립적으로 검증할 수 있다. 또한, 이를 통해 코드 변경 시 발생할 수 있는 버그를 조기에 발견하고 수정할 수 있다.

Given-When-Then 패턴의 이해

'Given-When-Then' 패턴은 테스트 코드를 작성할 때 가독성을 높이고 구조를 명확하게 하기 위해 자주 사용되는 방식이다. 이 패턴을 사용하면 테스트 코드의 흐름을 쉽게 이해할 수 있으며, 상황에 따른 동작과 결과를 명확히 구분할 수 있다. 나도 테스트 코드를 작성할 때 마다 이 패턴을 항상 사용하는 것 같다. 이 방식은 특히 BDD(Behavior Driven Development)에서 많이 사용된다.

  • Given: 테스트의 사전 조건을 설정하는 단계이다. 예를 들어, 필요한 객체를 생성하거나 데이터베이스에 기본 데이터를 저장하는 작업이 포함된다. 테스트를 실행하기 전에 필요한 모든 준비가 이 단계에서 이루어진다. 이는 테스트의 신뢰성을 높이기 위해 매우 중요하다.
  • When: 테스트 대상 메서드를 호출하거나 특정 동작을 수행하는 단계이다. Given 단계에서 설정된 조건을 바탕으로 실제로 테스트할 행동을 실행한다. 이 단계는 실제 기능의 핵심 부분을 다루며, 특정 입력이 주어졌을 때의 동작을 검증한다.
  • Then: 실행된 동작의 결과를 검증하는 단계이다. 기대하는 결과가 실제로 나타나는지 확인하고, 예상된 출력이나 상태가 제대로 반영되었는지 검증한다. 이 단계에서는 다양한 검증 메서드를 통해 기대하는 결과와 실제 결과를 비교한다.

예시: Given-When-Then 패턴의 사용

@Test
@DisplayName("냉장고 생성 - 요청이 성공하면 200 OK와 냉장고 응답 반환")
void createRefrigerator_ShouldReturnOkAndRefrigeratorResponse() throws Exception {
    // Given
    RefrigeratorCreateRequest request = new RefrigeratorCreateRequest(3L);

    // When & Then
    mockMvc.perform(post("/refrigerators")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.memberId").value(3L));
}

위의 테스트 코드에서 Given 단계는 RefrigeratorCreateRequest 객체를 생성하는 부분이며, When 단계는 mockMvc를 사용하여 POST 요청을 보내는 부분이다. 마지막으로 Then 단계에서 기대하는 응답 상태(200 OK)와 memberId 값이 3인지 확인하여 테스트가 성공적으로 완료되었는지를 검증한다.

이러한 패턴을 통해 테스트 코드를 쉽게 이해하고 유지 보수할 수 있으며, 다른 사람이 내 코드 리뷰를 해주거나 보았을 때 테스트의 목적과 기능을 빠르게 파악할 수 있다.

잘 작성된 테스트 코드의 요건

좋은 테스트 코드는 유지 보수성을 높이고 코드의 신뢰성을 보장한다. 그렇다면 좋은 테스트 코드란 어떤 것일까? 아래에 그 요건을 정리해 보았다.

  1. 명확성: 테스트 코드는 누구나 읽고 이해할 수 있어야 한다. 테스트하려는 기능과 검증하려는 결과가 명확히 드러나야 한다. 복잡한 로직을 간결하게 표현하고, 의미 있는 변수명을 사용하는 것이 중요하다.
  2. 독립성: 각각의 테스트는 서로 독립적이어야 한다. 테스트가 실행될 때 다른 테스트의 결과나 데이터에 의존하지 않아야 하며, 테스트 환경이 항상 초기 상태에서 시작되도록 보장해야 한다. 이를 위해 @BeforeEach@AfterEach를 통해 테스트 환경을 초기화할 수 있다.
  3. 신뢰성: 매번 동일한 결과를 반환해야 한다. 테스트를 실행할 때마다 결과가 달라지는 코드는 신뢰성이 떨어지며, 이는 테스트 코드의 기본 역할을 저해한다. 외부 요인에 의해 영향을 받지 않도록 Mock 객체를 사용하는 것도 좋은 방법이다.
  4. 유지 보수 용이성: 테스트 코드는 실제 코드의 변경 사항을 쉽게 반영할 수 있도록 설계되어야 하며, 테스트가 실패 했을 때 빠르게 수정할 수 있어야 한다. 이를 위해 테스트 코드 또한 리팩토링의 대상이 될 수 있다.
  5. 실제 시나리오 기반 테스트: 단순한 메서드의 결과를 검증하는 것 외에도, 실제 사용자가 애플리케이션을 사용하는 시나리오를 기반으로 테스트를 작성해야 한다. 이를 통해 예상하지 못한 오류나 버그를 사전에 방지할 수 있다.

테스트 코드 커버리지

테스트를 마무리하면 떠오르는 것이 바로 커버리지이다. 테스트 코드 커버리지(Test Code Coverage)는 작성한 테스트 코드가 실제 애플리케이션 코드의 몇 퍼센트를 테스트하고 있는지를 나타내는 지표이다. 보통 커버리지는 라인 커버리지분기 커버리지로 나뉜다.

  • 라인 커버리지: 코드의 각 라인이 테스트에 의해 실행되었는지를 측정한다. 일반적으로 라인 커버리지가 높을수록 많은 부분이 테스트되고 있다는 의미이다. 이 값이 높을수록 코드의 대부분이 테스트되었음을 의미하지만, 단순히 수치만 높이는 것이 아닌 실제적인 테스트가 필요하다.

  • 분기 커버리지: 조건문이나 분기문(if, switch 등)의 각 분기가 테스트에 의해 실행되었는지를 측정한다. 이는 코드의 다양한 경로에 대한 테스트를 의미하며, 모든 가능한 시나리오를 테스트하는 것이 목표이다. 예를 들어, 조건에 따라 다른 결과를 반환하는 메서드는 모든 조건을 테스트해야 한다.

테스트 커버리지 확인하는 법 (IntelliJ)

IntelliJ에서는 테스트 커버리지를 빠르게 확인할 수 있다.

  1. 테스트 클래스 실행: 먼저, 테스트하고자 하는 클래스나 메서드를 우클릭하여 실행 옵션을 선택한다. 이때 "Run with Coverage" 또는 "Coverage 실행"을 선택하면 된다.

  2. 커버리지 결과 확인: 테스트가 완료되면 IntelliJ 우측이나 하단에 커버리지 탭이 나타나고, 각 클래스와 메서드의 커버리지 결과를 색상으로 시각적으로 확인할 수 있다. 초록색은 테스트된 부분, 빨간색은 테스트되지 않은 부분을 나타낸다.

  3. 세부 내용 확인: 커버리지 탭에서 클래스나 메서드를 클릭하면, 코드 라인 단위로 어떤 부분이 테스트되었는지 세부적으로 확인할 수 있다. 보통 말하는 테스트 커버리지는 라인 커버리지를 많이 말하고 라인 커버리지가 80% 이상의 테스트 코드가 매우 잘 짠 테스트코드라고 많이들 이야기 한다.

테스트 커버리지를 확인하면서 실제 코드의 어느 부분이 충분히 테스트되지 않았는지 파악하고, 필요한 테스트를 추가함으로써 코드의 신뢰성을 높일 수 있다.

테스트 코드는 단순하게 많이 작성한다고 좋은 것이 아니라, 의미 있는 테스트를 작성하여 실제 서비스의 품질을 높이는 것이 중요하다. 예를 들어, 각 로직을 단순히 통과하는 것이 아니라, 오류가 발생할 수 있는 케이스를 충분히 검증하고 예외 상황을 처리하는 것이 좋은 테스트 코드이다.

테스트 코드 커버리지 높이기 위한 팁

  • 여러 경로에 대한 테스트: 가능한 모든 경로를 테스트해야 한다. 예를 들어, 성공 케이스뿐만 아니라 실패 케이스, 예외 케이스 등을 모두 포함시킨다. 이를 통해 예상치 못한 상황에서도 시스템이 안정적으로 동작하는지 확인할 수 있다.
  • 의미 있는 데이터 설정: 테스트에 사용되는 데이터가 실제 시나리오와 유사하게 설정되어야 한다. 예를 들어, 실제로 발생할 수 있는 다양한 입력 값에 대한 테스트를 작성하는 것이 좋다. 이를 통해 테스트는 실제 운영 환경에서도 신뢰할 수 있는 결과를 제공한다.
  • Mock 객체의 활용: 의존성이 높은 객체는 Mocking을 통해 독립적으로 테스트할 수 있다. 특히 데이터베이스나 외부 API와의 통신이 있는 경우 Mock을 사용하여 테스트 속도를 높이고 신뢰성을 확보할 수 있다.

테스트 코드의 예시 분석

@Test
@DisplayName("냉장고 조회 - 재료가 있는 냉장고를 조회할 때 재료 목록이 포함된다")
void getRefrigerator_WithIngredients_ShouldReturnRefrigeratorWithIngredients() throws Exception {
    // Given
    RefrigeratorRequest request = new RefrigeratorRequest(memberIdWithIngredients);

    // When & Then
    mockMvc.perform(get("/refrigerators")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.memberId").value(memberIdWithIngredients))
            .andExpect(jsonPath("$.ingredients[0].type").value("EGG"));
}

위의 코드에서는 재료가 있는 냉장고를 조회할 때 재료 목록이 포함되는지를 검증하고 있다. Given When Then 패턴에 적절한 코드가 들어있고 다양한 조건(재료가 있을 때, 없을 때)을 설정하여 실제 서비스에서 발생할 수 있는 여러 상황을 충분히 고려하고 있다. 이를 통해 코드가 예상치 못한 상황에서도 올바르게 동작하는지를 검증할 수 있다.

결론

Spring 애플리케이션에서 테스트 코드를 잘 작성하는 것은 단순한 코드 작성 이상의 의미를 가진다고 생각한다. TDD라는 개발 방식이 많이 유행하고 있는 것처럼 테스트 코드는 개발에서 빠질 수 없는 요소라고 본다.

테스트 코드를 작성하는 과정에서 발생할 수 있는 다양한 문제에 직면하면 더 나은 설계와 개선을 경험하여 더 나은 개발자가 될 수 있을 것이다. 이를 통해 여러분의 프로젝트에서도 이와 같은 테스트 코드를 작성해보며, 코드 품질과 유지 보수성을 높이는 경험을 해 보길 바란다. 테스트는 단순한 작업이 아닌, 전체 개발 주기에서 품질을 높이는 중요한 프로세스임을 항상 기억했으면 좋겠다.

여러분의 테스트 코드 작성 여정에 행운이 가득하길!!

profile
다재다능한 Backend 개발자에 도전하는 개발자

0개의 댓글