문제&해결

테스트 작성 부담감 극복하기 - POJO와 통합 테스트 중심의 전략

mint* 2024. 7. 5. 16:31
728x90

서론

저를 포함한 많은 개발하시는 분들이 테스트 코드 작성에 부담감을 느끼는 것 같습니다.
그 중 한가지 이유로 여러 종류의 테스트(통합 테스트, 컨트롤러, 서비스, 도메인 등)를 모두 작성해야 한다는 부담감 때문입니다.

 

이 글에서는 제가 테스트 코드를 작성하면서 오해했던 부분들과 공부를 통해 깨달은 점들을 공유하고자 합니다.
또한, 효과적인 테스트 작성을 위한 우선순위에 대해서도 작성해보았습니다. (부담감이 줄어들길 바라면서..!)

 

올바르지 않은 정보가 있을 수 있습니다. 댓글로 알려주시면 감사하겠습니다 🙇‍♀️

 

테스트에 대한 오해와 깨달음

    1. 테스트 기법에 대해 과도하게 집중했습니다.
      처음에는 다양한 테스트 기법을 배우는 데 집중했습니다.
      하지만 테스트를 작성하면서 깨달은 점은, 가장 중요한 것은 프로덕션 코드의 정확성을 검증하는 것이지 어렵고 복잡한 기법이 아니라는 점입니다.

    2. POJO 테스트의 중요성을 간과했습니다.
      처음에는 POJO(Plain Old Java Object) 테스트의 중요성을 간과했습니다.
      하지만 POJO 테스트, 특히 도메인 테스트가 비즈니스 로직의 핵심을 검증하는 가장 중요한 부분이라는 것을 깨달았습니다.

    3. 통합 테스트에 대해 오해했습니다,,
      처음에는 통합 테스트가 무겁고 비효율적이라고 생각해 Slice 테스트로 대체하려 했습니다.
      하지만 통합 테스트가 전체 시스템의 동작을 검증하는 데 중요하다는 것을 알게 되었습니다.

    4. 과도한 모킹(Mocking)을 수행했습니다.
      의존성을 최대한 분리하기 위해 과도하게 모킹을 사용했습니다.
      그러나 이로 인해 테스트의 복잡성만 증가 되고, 실제 프로덕션 환경과의 괴리를 만든다는 것을 깨달았습니다.
@Test
void testProcessOrderWithExcessiveMocking() {
 // given
 OrderRepository mockOrderRepo = mock(OrderRepository.class);
 PaymentService mockPaymentService = mock(PaymentService.class);
 Order order = new Order(1L, "Product A", 2, 100.0);

 given(mockOrderRepo.findById(1L)).willReturn(Optional.of(order));
 given(mockPaymentService.processPayment(any())).willReturn(true);

 OrderService orderService = new OrderService(mockOrderRepo, mockPaymentService);

 // when
 boolean result = orderService.processOrder(1L);

 // then
 then(result).isTrue();
 then(mockPaymentService).should().processPayment(any());
}
mocking은 외부 시스템의 의존성이 있는 서비스에 사용하는 것을 추천드립니다.

 

테스트 우선순위

위와 같은 생각을 하면서 어떤 테스트에 더 집중하고, 덜 집중하면 좋을지 생각해보았습니다.

물론 모든 테스트는 중요합니다. 어떤 테스트에 시간을 더 쏟으면 좋을지로 이해해주시면 감사하겠습니다.

 

1. 도메인(POJO) 테스트

  • 비즈니스 로직의 정확성을 보장하기 때문에 가장 중요합니다.
  • 외부 의존성 없이 순수한 도메인 로직만을 테스트합니다.
  • 테스트 실행 속도가 빠릅니다.
public class FoodTest {

    @Test
    public void 음식_생성_성공() {
        // when
        Food pizza = new Food("피자", 15000);

        // then
        then(pizza.getName()).isEqualTo("피자");
        then(pizza.getPrice()).isEqualTo(15000);
    }

    @Test
    public void 가격_인상_성공() {
        // given
        Food burger = new Food("햄버거", 8000);

        // when
        burger.increasePrice(1000);

        // then
        then(burger.getPrice()).isEqualTo(9000);
    }
}

 

2. 통합 테스트

  • 실제 운영 환경과 유사한 환경에서 테스트할 수 있어 중요합니다.
  • end-to-end 동작을 확인합니다.
public class FoodApiTest extends IntegrationTestSupport {
    @Test
    public void 음식등록_테스트() throws Exception {
        final String requestBody = readJson("/food-registration.json");
        this.mockMvc.perform(
                post("/foods")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(requestBody)
            )
            .andExpect(status().isCreated());
    }
}

 

3. 선택적인 추가 테스트

상황에 따라 필요한 경우 추가하면 좋을 것 같습니다.

a) Slice 테스트 (예: @WebMvcTest, @DataJpaTest)

  • 애플리케이션의 특정 "슬라이스" 또는 레이어만을 테스트합니다.
  • @WebMvcTest : 웹 레이어(컨트롤러)만을 테스트
  • @DataJpaTest : JPA 컴포넌트(리포지토리)만을 테스트
@WebMvcTest(FoodController.class)
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
class FoodControllerTest {
    private final MockMvc mockMvc;

    @MockBean
    private FoodService foodService;

    FoodControllerTest(MockMvc mockMvc) {
        this.mockMvc = mockMvc;
    }

    @Test
    void 음식_조회_성공() throws Exception {
        // given
        given(foodService.getFood(1L)).willReturn(new Food(1L, "피자", 15000));

        // when
        ResultActions result = mockMvc.perform(get("/foods/1"));

        // then
        then(result)
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("피자"))
            .andExpect(jsonPath("$.price").value(15000))
    }
}

 

b) 서비스 Mock 테스트

    • 서비스 레이어의 로직을 단위 테스트 합니다.
    • 서비스가 의존하는 다른 컴포넌트(예: 리포지토리)를 모의(mock) 객체로 대체합니다.
      • Mockito 프레임워크를 주로 사용합니다.
pojo라고 볼수도 있지만, 의존하는 컴포넌트를 mocking하므로 pojo와 분리해서 작성했습니다.
@ExtendWith(MockitoExtension.class)
class FoodServiceTest {
    @Mock
    private FoodRepository foodRepository;

    @InjectMocks
    private FoodService foodService;

    @Test
    void 음식_조회_성공() {
        // given
        Food expectedFood = new Food(1L, "치킨", 18000);
        given(foodRepository.findById(1L)).willReturn(Optional.of(expectedFood));

        // when
        Food actualFood = foodService.getFood(1L);

        // then
        then(actualFood).isEqualTo(expectedFood);
        verify(foodRepository).findById(1L);
    }
}

 

결론

테스트 작성의 핵심은 아래 두 가지에 집중하는 것입니다.
1, 비즈니스 로직이 정확하게 동작하는지 검증 (POJO)
2. 시스템 전체가 end-to-end로 올바르게 동작하는지 검증(통합 테스트)
이 두가지를 중심으로 테스트를 작성하고, 필요에 따라 추가적으로 작성해보는 것을 제안드립니다.

 

테스트 코드가 많을 수록 좋다고 생각할 수 있지만(다다익선?) 중복된 검증이나 과도한 테스트는 오히려 유지 보수 비용을 증가시킬 수 있다고 생각합니다.
따라서 테스트의 양보다는 질과 효율성에 초점을 맞추는 것이 중요합니다.

 

이 글이 저처럼 테스트 코드에 막연한 부담감과 두려움을 가진 분들에게 도움이 되었으면 좋겠습니다.
결국,, 테스트 코드 작성은 선택이 아닌 필수이기 때문입니다. 😂

읽어주셔서 감사합니다.

 

정확하지 않은 부분이 있고 배우는 중이니 잘못된 부분은 알려주시면 감사하겠습니다 !! 🙇‍♀️

728x90