본문으로 건너뛰기

Why-Integration-Testing

1 통합 테스트를 하는 이유

  • 단위 테스트가 비즈니스 로직을 확인하는 데 좋지만 비즈니스 로직을 외부와 단절된 상태로 확인하는 것으로 충분하지 않다
  • 각 부분이 데이터베이스나 메시지 버스 등의 외부 시스템과 어떻게 통합되는지 확인해야 한다

2 통합 테스트는 무엇인가?

  • 통합 테스트가 무엇인지 알기 위해선 먼저 단위 테스트가 무엇인지 알아야한다

2.1 단위 테스트란?

  • 단위 테스트는 다음 세 가지 요구 사항을 충족하는 테스트다
    • 단일 동작 단위를 검증하고
    • 빠르게 수행하고
    • 다른 테스트와 별도로 처리한다

2.2 통합 테스트란?

  • 단위 테스트의 3 가지 요구 사항 중 하나라도 충족하지 못하는 테스트는 통합 테스트 범주에 속한다
  • 따라서 단위 테스트가 아닌 모든 테스트가 통합 테스트에 해당된다
  • 실제로 통합 테스트는 시스템이 프로세스 외부 의존성과 통합해 어떻게 동작하는지 검증한다

단위 테스트와 통합 테스트 비교

  • 단위 테스트는 도메인 모델을 다루는 반면, 통합 테스트는 프로세스 외부 의존성과 도메인 모델을 연결하는 코드를 확인한다

2.3 테스트 피라미드

  • 단위 테스트와 통합 테스트 간의 균형을 유지하는 것이 중요하다
  • 균형을 유지한다는 것은 단위 테스트의 개수와 통합 테스트 개수의 균형을 맞추는 것

2.3.1 통합 테스트의 장단점

통합 테스트의 장점

  • 코드를 더 많이 실행시켜 회귀 방지가 단위 테스트보다 우수하다
    • 애플리케이션 코드, 라이브러리 코드를 모두 포함하기 때문
  • 제품 코드와의 결합도가 낮아 리팩터링 내성도 우수하다

통합 테스트의 단점

  • 통합 테스트가 외부 의존성에 직접 작동하며 느려지며, 이러한 테스트는 유지비가 많이 든다
  • 유지비 증가의 이유
    • 프로세스 외부 의존성 운영이 필요
    • 관련된 협력자가 많아 테스트가 비대해짐

2.3.2 단위 테스트의 장단점

단위 테스트의 장점

  • 빠른 피드백이 가능하며 유지비가 적게 든다

단위 테스트의 단점

  • 통합 테스트와 비교해 회귀 방지와 리팩터링 내성이 떨어진다

2.3.3 테스트 비율

  • 단위 테스트와 통합 테스트의 비율은 프로젝트의 특성에 따라 다를 수 있지만 일반적으로 아래와 같다
    • 단위 테스트로 가능한 많은 비즈니스 시나리오의 예외 상황을 확인한다
    • 통합 테스트는 주요 흐름과 단위 테스트가 다루지 못하는 기타 예외 상황을 다룬다
      • 주요 흐름은 시나리오의 성공적인 실행을 의미한다
      • 예외 사항은 비즈니스 시나리오 수행 중 오류가 발생하는 경우다
  • 대부분을 단위 테스트로 전환하면 유지비를 절감할 수 있다
  • 중요한 통합 테스트가 비즈니스 시나리오당 하나 또는 두 개 있으면 시스템 전체의 정확도를 보장할 수 있다
  • 이 지침을 따르면 테스트 피라미드의 형태가 나타날 것이다

2.2 통합 테스트와 빠른 실패

  • 통합 테스트에서 프로세스 외부 의존성과의 상호 작용을 모두 확인하려면 가장 긴 주요 흐름을 선택해야한다
    • 가장 긴 주요 흐름은 모든 프로세스 외부 의존성을 거치는 것이다
  • 이렇게 모든 상호 작용을 거치는 흐름이 없으면, 외부 시스템과의 통신을 모두 확인하는 데 필요한 만큼 통합 테스트를 추가로 작성하라

3 어떤 프로세스 외부 의존성을 직접 테스트하나?

  • 통합 테스트는 시스템이 프로세스 외부 의존성과 어떻게 통합하는지를 검증한다
  • 이러한 검증을 구현하는 방식은 두 가지가 있다
    • 실제 프로세스 외부 의존성을 사용하기
    • 목으로 대체하기
  • 두 가지 방식을 각각 언제 적용해야 될까?

3.1 프로세스 외부 의존성의 두 가지 유형

  • 모든 프로세스 외부 의존성은 두 가지 범주로 나뉜다
    • 관리 의존성
    • 비관리 의존성
  • 관리 의존성은 실제 인스턴스를 사용하고 비관리 의존성은 목으로 대체하라



3.1.1 관리 의존성

  • 전체를 제어할 수 있는 프로세스 외부 의존성을 관리 의존성이라 한다
  • 이러한 의존성은 애플리케이션을 통해서만 접근할 수 있다
    • 따라서 해당 의존성과의 상호 작용은 외부 환경에서 볼 수 없다
  • 좋은 예로 애플리케이션 데이터베이스가 있는데 이는 애플리케이션만 사용하는 데이터베이스르 말한다
    • 따라서 외부 시스템은 데이터베이스에 직접 접근하지 않고 애플리케이션에서 제공하는 API를 통해 접근한다
  • 관리 의존성과의 통신은 구현 세부사항이다
  • 관리 의존성과 통신하는 것은 애플리케이션뿐이므로 하위 호환성을 유지할 필요가 없다
    • 하위 호환성을 유지할 필요가 없다는 말은 외부 클라이언트는 데이터베이스를 어떻게 구성하는지 신경 쓰지 않는다는 것을 의미한다
    • 따라서 기존 기능을 손상시키지 않고 애플리케이션과 애플리케이션 데이터베이스 간의 통신 패턴을 원하는 대로 수정할 수 있다
    • 애플리케이션의 클라이언트의 시야에서 완전히 숨어있기 때문에 이와 같은 일이 가능하다
  • 관리 의존성은 통합 테스트에서 해당 의존성을 그대로 사용하라
  • Mocks-And-Test-Fragility.md 참조

3.1.2 비관리 의존성

  • 전체를 제어할 수 없는 프로세스 외부 의존성
  • 해당 의존성과의 상호 작용을 외부에서 볼 수 있다
  • 예를 들어 SMTP 서버와 메시지 버스 등이 있다
    • 둘 다 다른 애플리케이션에서 볼 수 있는 부작용을 발생시킴
  • 비관리 의존성과의 통신은 시스템의 식별할 수 있는 동작이다
    • 해당 의존성은 목으로 대체해야 한다

3.1.3 통합 테스트에서 실제 데이터베이스를 사용할 수 없는 경우

  • 통합 테스트에서 관리 의존성을 실제 버전으로 사용할 수 없는 경우도 있다
  • 관리 의존성임에도 불구하고 목으로 처리해야할까?
  • 관리 의존성을 목으로 대체하면
    • 리팩터링 내성이 저하된다
    • 회귀 방지도 떨어진다
    • 데이터베이스가 프로젝트의 유일한 프로세스 외부 의존성이면 회귀 방지에 있어 단위 테스트와 다를 바 없다
    • 회귀 방지가 떨어진다는 것은 실행 되는 코드의 양이 준다는 것과 같다 데이터베이스를 목으로 대체하면 컨트롤러에 코드 몇줄만 실행될 것
  • 따라서 통합 테스트에서 실제 데이터베이스를 사용할 수 없는 경우 아예 테스트를 작성하지 말고 도메인 모델의 단위 테스트에 집중하라
  • 가치가 충분하지 않은 테스트는 테스트 스위트에 있어서는 안 된다

4 의존성 추상화를 위한 인터페이스 사용

  • 단위 테스트 영역에서 가장 많이 오해하는 주제 중 하나는 인터페이스 사용이다
  • 개발자들은 인테페이스를 둔 이유를 자주 잘못 설명하고 인터페이스를 남용하는 경향이 있다

4.1 인터페이스와 느슨한 결합

  • 많은 개발자가 데이터베이스나 메시지 버스와 같은 프로세스 외부 의존성을 위해 인터페이스를 도입한다
  • 심지어 인터페이스의 구현이 하나만 있는 경우도 있다

구현이 하나인 인터페이스를 사용하는 이유

  • 구현이 하나인 인터페이스를 사용하는 일반적 이유는 인터페이스가
    • 프로세스 외부 의존성을 추상화해 느슨한 결합을 달성하고
    • 기존 코드를 변경하지 않고 새로운 기능을 추가해 공개 폐쇄 원칙을 지키키 때문이다
  • 이 두가지 이유 모두 오해다

구현이 하나인 인터페이스를 사용하면 안되는 이유

  • 단일 구현을 위한 인터페이스는 추상화가 아니며 해당 인터페이스를 구현하는 구체 클래스보다 결합도가 낮지 않다
    • 진정한 추상화는 발견하는 것이지, 발명하는 것이 아니다
    • 의미상 추상화가 이미 존재하지만 코드에서 아직 명확하게 정의되지 않았을 때 그 이후에 발견되는 것이다
    • 따라서 인터페이스가 진정으로 추상화되려면 구현이 적어도 두 가지는 있어야 한다
  • OCP원칙 보다 더 기본적인 원칙인 YAGNI(You aren't gonna need it)을 위반하기 때문에 잘못된 생각이다
    • YAGNI은 현재 필요하지 않은 기능에 시간을 들이지 말라는 것이다

4.2 프로세스 외부 의존성에 인터페이스를 사용하려는 이유

  • 인터페이스에 구현이 하나만 있다고 가정할 때 프로세스 외부 의존성에 인터페이스를 사용하는 이유는 무엇일까?
  • 간단히 말하자면 목을 사용하기 위함이다
  • 인터페이스가 없으면 테스트 대역을 만들 수 없으므로 테스트 대상 시스템과 프로세스 외부 의존성 간의 상호 작용을 확인할 수 없다
  • 따라서 프로세스 외부 의존성을 목으로 처리할 필요가 없으면 인터페이스를 사용하지 마라
  • 비관리 의존성만 목으로 처리하므로 비관리 의존성에 대해서만 인터페이스를 사용하면 된다
  • 관리 의존성은 구체 클래스로 사용하라
    • 구현이 하나인 경우 목 대체를 이유로 인터페이스를 도입하지 말고 구체 클래스를 사용하라

4.3 프로세스 내부 의존성을 위한 인터페이스 사용

  • 프로세스 외부 의존성과 마찬가지로 구현이 하나만 있는 인터페이스 도입은 목을 처리하기 위해서다
  • 그러나 프로세스 내부 의존성을 목으로 대체하여 상호 작용을 확인하면 깨지기 쉬운 테스트로 이어지고 리팩터링 내성이 떨어진다
  • 따라서 프로세스 내부 의존성을 목을 처리하기 위해 구현이 하나인 인터페이스를 사용하지 말고 구체 클래스를 사용하자

5 통합 테스트 모범 사례

  • 통합 테스트를 최대한 활용하는 일반적인 지침
    • 도메인 모델 경계 명시하기
    • 애플래케이션 내 계층 줄이기
    • 순환 의존성 제거하기

5.1 도메인 모델 경계 명시하기

  • 도메인 모델을 코드베이스에서 명시적이고 잘 알려진 위치에 두자
  • 도메인 모델은 프로젝트가 해결하고자하는 문제에 대한 도메인 지식의 모음이다
  • 단위 테스트는 도메인 모델과 알고리즘을 대상으로 하며 통합 테스트는 컨트롤러를 대상으로 한다
  • 도메인 클래스와 컨트롤러 사이의 명확한 경계로 단위 테스트와 통합 테스트의 대상을 명확히 구분할 수 있다

5.2 계층 수 줄이기

  • 대부분의 프로그래머들은 간접 계층을 추가하여 코드를 추상화하려고 한다

너무 많은 계층의 문제점

  • 극단적인 경우로 추상 계층이 너무 많으면 코드베이스를 탐색하기 어렵고 간단한 연산이라도 숨은 로직을 이해하기 어려워진다
  • 추상화가 지나치게 많으면 단위 테스트와 통합 테스트에 도움이 되지 않는다
  • 간접 계층이 많은 코드베이스는 컨트롤러와 도메인 모델 사이 명확한 경계가 없는 편이다
  • 각 계층을 따로 검증하는 경향이 훨씬 강해진다
    • 각 테스트는 특정 계층의 코드만 실행하고 하위 계층은 목으로 처리한다
    • 결과적으로 낮은 리팩터링 내성과 불충분한 회귀 방지를 초래한다

해결책

  • 가능한 한 간접 계층을 적게 사용하라
  • 대부분의 백엔드 시스템에서는 도메인 모델, 애플리케이션 서비스, 인프라 계층만 활용하면 된다

도메인 모델

  • 비즈니스 로직

애플리케이션 서비스 계층(컨트롤러 계층)

  • 외부 클라이언트의 진입점 제공 및 도메인 클래스와 프로세스 외부 의존성 간의 작업 조정

인프라 계층

  • 도메인 모델에 속하지 않는 알고리즘
  • 프로세스 외부 의존성에 접근할 수 있는 코드로 구성됨
  • 데이터베이스 저장소, ORM매핑, SMTP 게이트

5.3 순환 의존성 제거하기

5.4 테스트에서 다중 실행 구절 사용

  • 테스트에서 두 개 이상의 준비나 실행 또는 검증 구절을 두는 것은 코드 악취에 해당한다
  • 테스트가 여러 가지 동작 단위를 한번에 확이해 유지 보수성을 저해한다는 신호다
  • 이런 경우 각 실행을 고유의 테스트로 추출해 테스트를 나누는 것이 좋다

예외 사항

  • 외부 의존성을 관리하기 어려운 경우 둘 이상의 실행구절을 작성할 수 있다
  • 예를 들어 사용자를 등록하면 외부 은행 시스템에서 은행 계좌가 만들어진다고 하자
    • 은행에서 샌드박스를 제공하기에 엔드 투 엔드 테스트에서 이 샌드박스를 사용한다
    • 문제는 샌드박스가 너무 느리거나 호출 수를 제한한다는 것
    • 이러한 시나리오에서 여러 동작을 하나의 테스트로 묶을 수 있다
  • 단 단위 테스트는 프로세스 외부 의존성으로 작동하지 않기 때문에 절대로 실행 구절이 여러 개 있어서는 안 된다
  • 통합 테스트조차 실행을 여러 단계로 하는 경우는 드물다
  • 실제로 다단계 테스트는 거의 엔드 투 엔드 테스트 범주에 속한다