업데이트:

회사에서 테스트 코드 작성에 대해 가이드를 하던 중 “단위 테스트를 해야하는 이유”에 대해 설명할 일이 있어 관련 글을 포스팅해보려 합니다.

제가 공부한 내용도 있지만, 제가 테스트 코드를 작성하면서 생각하게 된 주관적인 내용이 대부분이기 때문이 이를 고려해주시면서

글을 읽어주시기 바랍니다

테스트 종류

Test-pyramid

위의 이미지는 테스트의 종류를 나타내고 각각의 비중을 나타내고 있습니다.

인수 테스트 (E2E Test)

  • End to End Test
  • 실제 유저 시나리오대로 API 호출 등을 통해 서버의 End-point(보통 API)를 호출해 테스트를 합니다.

통합 테스트 (Integration Test)

  • 여러 컴포넌트의 동작을 합쳐서 테스트를 합니다.
  • 외부 라이브러리, DB 연동, API 호출 등 다양한 연동 포인트와 함께 테스트를 진행하는 것을 말합니다.
  • 다른 환경과 엮이기 때문에 내 시스템만의 테스트에 대한 신뢰/보증이 어렵습니다.

단위 테스트 (Unit Test)

  • 개별 컴포넌트의 동작을 테스트
  • 범위에 관해 정해져있진 않지만, 일반적으로 클래스의 메소드 단위로 작성합니다.
  • 작은 단위로 테스트 코드를 작성하기 때문에 디버깅이 쉬워지고, 복잡성이 낮아집니다.
  • 배포하기 전에 build 과정에서 자동화에 많이 사용합니다.

구글에선는 70% 단위테스트, 20% 통합 테스트, 10% 인수 테스트의 비율을 유지하는 것이 이상적이라고 합니다.

여기서 알 수 있듯이 단위 테스트의 비율이 월등히 높은데요)

이유가 무엇일까요?

지금부터는 우리가 왜 통합 테스트 보단 단위 테스트를 작성해야 하는가에 대해 초점을 맞추고

통합 테스트가 어떤 문제점이 있는지 얘기해보겠습니다.

통합 테스트 코드의 단점


1. 안정성의 신뢰가 떨어진다.

테스트 코드의 목적은 리팩토링, 클린코드, 설계, TDD 등 여러가지가 있겠지만, 기능 점검이 가장 근본적인 목적일 것입니다.

그렇다면 통합 테스트는 과연 기능 점검을 완전 신뢰할 수 있을까요?

예를 들어 “주문하기”라는 하나의 비즈니스 플로우를 통합 테스트 코드를 작성하고 기능 점검을 했을 때,

주문한 상품의 갯수만큼 재고를 체크하는 로직이 있다고 가정해보겠습니다.

OrderService

@RequiredArgsConstructor
public class OrderService {
	
    private final StockRepository stockRepository;

    public OrderResponseDto order(OrderRequestDto request) {
		
        Stock itemStock = stockRepository.findById(request.getItemId);

        if (itemStock.isNotExistStock(request.getOrderItemCount)) {
            throw new IllegalArgumentException("주문한 상품의 재고가 부족합니다.");
        }

        /* 이후 비즈니스 로직 */

    }

}

💡저는 통합 테스트 코드는 성공 케이스를 위주로 작성하는 것이 낫다고 보고 있습니다. 그 이유는 비즈니스 플로우에서 수 많은 케이스가 발생하는 데, 그 모든 케이스를 통합 테스트로 작성하기에는 양이 너무 많아지고, 모든 케이스를 통합 테스트코드로 짤바에는 개별 단위로 쪼개어 단위 테스트가 훨씬 낫다고 보고 있습니다. 그래서 이번 예시에서도 통합 테스트 코드를 성공 케이스만 작성했다는 것을 전제로 설명하겠습니다.

위와 같은 Service에서 StockisNotExistStock를 항상 true로 리턴하도록 누군가 실수로 수정했다고 했을 때

과연 통합 테스트 코드가 이를 체크할 수 있을까요?

보통 자동화를 통해 테스트가 성공이 떨어지면 내부를 꼼꼼히 뜯어보지 않을 확률이 높기 때문에 이런 버그를 모르고 지나칠 확률이 높습니다.

조금 극단적인 예를 들었지만 통합 테스트는 이 처럼 내부의 세밀한 기능 점검을 당연하게도 못할 확률이 높습니다.

2. 다른 컴포넌트로 인한 테스트 실패 가능성이 있다.


통합 테스트는 아시듯이 다양한 컴포넌트들의 동작을 합쳐서 테스트하는 것입니다.

그런데 이런 통합 테스트에서 다른 외부 컴포넌트의 문제가 발생했다면 어떻게 될까요?

예를 들어 외부 API를 통신하는 부분에서 갑자기 네트워크 상에 문제가 발생하여 502 bad gateway를 내려준다면 당연히 테스트는 실패할 것입니다.

하지만 이를 과연 테스트 실패로 봐야할까요?

이렇듯 통합 테스트는 다른 요인으로 인해 테스트가 깨질 가능성이 있어 통합 테스트 코드를 완전히 신뢰하기 어렵습니다.

3. 사전 세팅을 관리하기 어렵다.


💡저는 Spring 사용자이기 때문에 그와 관련된 기술 내용만을 다루는 점 양해 바랍니다.

통합 테스트는 다양한 컴포넌트와 연동을 하기 때문에 여러 사전 준비가 필요합니다.

예를 들면 DB 커넥션이 있겠죠.

주로 Spring에서 통합 테스트 코드를 작성할 때 @SpringBootTest 어노테이션을 많이 사용하는데

@SpringBootTest을 사용하면 Property 파일 로드, DB 커넥션, 다른 외부 서버와 커넥션 등 다양하게 세팅해야 될 부분이 있습니다.

사전 세팅하는 시간도 오래걸리고, 고려해야할 점이 많다는 것도 문제지만 더 큰 문제는 따로 있습니다.

이런 사전 세팅하는 부분들은 여러 테스트 케이스에 걸쳐 있기 때문에 하나의 테스트를 수정함에 따라 다른 테스트에도 영향을 줄 수 있다는 점입니다.

통합 테스트를 위한 Base 통합 테스트 클래스를 세팅해놓고, 여러 테스트 클래스에서 이를 상속받아 사용하고 있다고 이를 가정해보겠습니다.

💡이렇게 테스트를 할 때 기본 베이스가 되는 것들을 Fixture라고 합니다. 저는 예시로 MockBean을 들었지만, 데이터 세팅, DB 커넥션 부분 등도 예시가 될 수 있습니다.

BaseIntegrationTest

@ActiveProfile(ProfileType.TEST)
@SpringBootTest
public class BaseIntegrationTest {

    @MockBean
    private NotificationService notificationService;

}
public ATest extends BaseIntegrationTest {

}
public BTest extends BaseIntegrationTest {

}

이때, 알림에 관한 NotificationServiceMockBean으로 선언하자고 팀 내부 회의를 통해 정책이 잡혔었는데,

추후 BTest에서 알림에 관한 테스트 코드를 작성해야할 일이 생겼다면 어떻게 해야 할까요?

BaseIntegrationTest를 수정해야할테고, 이는 ATest에 영향을 주게 될 것입니다.

그렇다고 모든 통합 테스트 코드마다 ATest, BTest, CTest 모든 테스트 클래스별로 사전 세팅 부분을 쪼개자니 낭비가 심해질 것입니다.

이 처럼 통합 테스트 코드의 사전 세팅은 여러 개를 만들자니 낭비가 심하고, 또 하나로 합치자니 관리의 유연성이 떨어집니다.

4. 유지보수가 어렵다.


통합 테스트 코드는 하나의 비즈니스 플로우에 얽힌 코드가 변경될때 마다 영향이 있을 수밖에 없습니다.

하나의 플로우에 수많은 요구사항이 추가되고, 변경되는 일이 매일 같이 발생하는 데

이를 모두 커버할 수 있는 통합 테스트 코드는 이 세상 어디에도 존재하지 않는다고 생각합니다.

만약 특정 하나의 메소드에서 if문 하나 바꾸는데, 통합 테스트 전부를 신경써야 한다면 어떨까요?

엄청난 시간 및 비용의 낭비가 예상될 것입니다.

5. 디버깅이 어렵다.


통합 테스트는 디버깅 역시 힘든데 비즈니스 로직에서 특정 코드를 변경했다고 가정해보겠습니다.

Controller → Service → Domain → Repository 예를 들어 앞에 같은 상황일 때 Domain의 코드를 변경했는데

이 사이드 이펙트가 Service에서 발생한다면??

당연히 테스트는 실패하고 버그를 찾으려 하겠지만, 어디서 발생한 버그인지 찾기 위해 추적 시간이 필요할 것입니다.

마무리


통합 테스트 코드의 단점에 대해만 나열해봤는데 통합 테스트 코드도 분명히 필요하다고는 생각합니다.

예를 들면 Application 아키텍처의 변경이나 큰 리팩토링이 있을 시 기존에 잘 돌아가던 시나리오 대로 통합 테스트 코드를 활용할 수 있습니다.

아키텍처가 변경되는 것이지 기능의 추가/삭제/수정이 아니니까요.

하지만 만약 테스트 코드가 전무하거나 부족하다면 단위 테스트가 선행되고 추후 필요 시 마다 통합 테스트 코드를 작성하는 편이

관리 및 유지보수, 테스트 코드 활용 면에서 훨씬 더 좋다고 생각합니다.

오늘의 포스트는 통합 테스트 코드의 단점만을 나열하게 되었는데, 다음 포스트 단위 테스트에 대해서 작성해보도록 하겠습니다.

오늘도 긴 제 글을 읽어 주셔서 감사합니다😄

댓글남기기