• 단위 테스트에만 전적으로 의존하면 시스템이 전체적으로 잘 작동하는지 확신할 수 없다.

통합 테스트는 무엇인가?

통합 테스트의 역할

  • 아래 단위 테스트 세 가지 요구 사항중 하나라도 충족하지 못하면 통합 테스트의 범주에 속한다.
    • 단일 동작 단위를 검증하고,
    • 빠르게 수행하고,
    • 다른 테스트와 별도로 처리한다.
  • 단위 테스트는 도메인 모델을 다루는 반면, 통합 테스트는 프로세스 외부 의존성과 도메인 모델을 연결하는 코드(컨트롤러)를 확인한다.

다시 보는 테스트 피라미드

  • 통합 테스트는 유지비가 많이 든다.
    • 프로세스 외부 의존성 운영이 필요함
    • 관련된 협력자가 많아서 테스트가 비대해짐
  • 단위 테스트로 가능한 한 많이 비즈니스 시나리오의 예외 상황을 확인하고, 통합 테스트는 주요 흐름(happy path)과 단위 테스트가 다루지 못하는 기타 예외 상황을 다룬다.
  • 대부분을 단위 테스트로 전환하면 유지비를 절감할 수 있고, 단위 테스트와 통합 테스트 사이의 피라미드 같은 비율을 만든다.
  • 복잡도가 낮은 단순 애플리케이션은 도메인 모델과 알고리즘 사분면에 코드가 거의 없어, 피라미드 대신 작사각형 모양이 된다.

통합 테스트와 빠른 실패

  • 통합 테스트에서 프로세스 외부 의존성과 상호 작용을 모두 확인하려면 가장 긴 주요 흐름을 선택하라.
  • 이렇게 모든 상호 작용을 거치는 흐름이 없으면, 외부 시스템과의 통신을 모두 확인하는 데 필요한 만큼 통합 테스트를 추가로 작성하라.
  • 어떤 예외 상황에 잘못 실행돼 전체 애플리케이션이 즉시 실패하면 해당 예외 상황은 테스트할 필요가 없다.
    • 좋지 않은 테스트를 작성하는 것보다는 테스트를 작성하지 않는 것이 좋다.
  • 빠른 실패 원칙: 예기치 않는 오류가 발생하자마자 현재 연산을 중단하는 것을 의미한다. 이 원칙은 다음을 통해 애플리케이션의 안정성을 높인다.
    • 피드백 루프 단축: 버그를 빨리 발견할수록 더 쉽게 해결할 수 있다. 이미 운영 환경으로 넘어온 버그는 개발 중에 발견된 버그보다 수정 비용이 훨씬 더 크다.
    • 지속성 상태 보호: 버그는 애플리케이션 상태를 손상시킨다. 손상된 상태가 데이터베이스로 침투하면, 고치기가 훨씬 어려워진다. 빨리 실패하면 손상이 확산되는 것을 막을 수 있다.

어떤 프로세스 외부 의존성을 직접 테스트해야 하는가?

  • 외부 의존성 검증 구현 방식 두 가지
    • 실제 프로세스 외부 의존성을 사용한다.
    • 목으로 대체한다.

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

  • 프로세스 외부 의존성은 두 가지 범주로 나뉜다.
    • 관리 의존성(전체를 제어할 수 있는 프로세스 외부 의존성): 애플리케이션을 통해서만 접근할 수 있으며, 해당 의존성과의 상호 작용은 외부 환경에서 볼 수 없다.
      • 예: 데이터베이스 등
    • 비관리 의존성(전체를 제어할 수 없는 프로세스 외부 의존성): 해당 의존성과의 상호 작용을 외부에서 볼 수 있다.
      • 예: SMTP 서버, 메시지 버스 등
  • 관리 의존성은 실제 인스턴스를 사용하고, 비관리 의존성은 목으로 대체한다.
    • 관리 의존성과의 통신은 구현 세부 사항이다.
    • 비관리 의존성과의 통신은 식별할 수 있는 동작이다.

관리 의존성이면서 비관리 의존성인 프로세스 외부 의존성 다루기

  • 관리 의존성과 비관리 의존성 모두의 속성을 나타내는 프로세스 외부 의존성의 예: 다른 애플리케이션이 접근할 수 있는 데이터베이스
    • 이러한 경우는 DB테이블이 사실상 메시지 버스 역할이고, 각 행이 메시지 역할을 한다.
    • 테이블을 관리, 비관리 부분으로 나누고 비관리에 해당하는 테이블을 이용한 통신 패턴에는 목을 사용한다.

통합 테스트에서 실제 데이터베이스를 사용할 수 없으면 어떻게 할까?

  • 보안 정책 때문이거나 비용 문제 등으로, 통합 테스트에서 관리 의존성을 실제로 사용할 수 없는 경우도 있다.
  • 이런 경우 통합 테스트를 아예 작성하지 말고 도메인 모델의 단위 테스트에만 집중하라.
    • 관리 의존성을 목으로 대체하면 통합 테스트의 리팩터링 내성이 저하되기 때문이다.

통합 테스트: 예제

엔드 투 엔드 테스트는 어떤가?

  • 엔드 투 엔드 테스트는 외부 클라이언트를 모방하므로, 테스트 범위에 포함된 모든 프로세스 외부 의존성을 참조하는 배포된 버전의 애플리케이션을 테스트한다.
  • 엔드 투 엔드 테스트는 관리 의존성을 직접 확인해서는 안되고, 애플리케이션을 통해 간접적으로 확인해야 한다.

통합 테스트: 첫 번째 시도

  • 준비 구절에서 데이터를 데이터베이스에 직접 삽입하지 않고, 헬퍼 메서드를 만들어 호출한다.
    • 이러한 메서드는 여러 통합 테스트에서 재사용할 수 있다.

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

인터페이스와 느슨한 결합

  • 인터페이스를 사용하는 일반적인 이유
    • 프로세스 외부 의존성을 추상화해 느슨한 결합을 만든다.
    • 기존 코드를 변경하지 않고 새로운 기능을 추가해 OCP를 지키기 때문이다.
  • 만약 구현이 하나만 있는 인터페이스는 위 2가지 이유를 달성하지 못한다.
    • 단일 구현을 위한 인터페이스는 추상화가 아니며, 해당 인터페이스를 구현하는 구체 클래스보다 결합도가 낮지 않다. 추상화는 발견하는 것이지, 발명하는 것이 아니다.
    • YAGNI를 위반한다. YAGNI는 현재 필요하지 않는 기능에 시간을 들이지 말라는 것이다. YAGNI를 지켜야되는 이유는 다음과같다.
      • 현재 비즈니스 담당자들에게 필요하지 않은 기능에 시간을 보낸다면, 지금 당장 필요한 기능을 제치고 시간을 허비하는 것이다.
      • 프로젝트 코드는 적을수록 좋다. 요구 사항이 바로 있는 경우가 아닌데도 만일을 위해 코드를 작성하면 코드베이스의 소유 비용이 불필요하게 늘어난다.

프로세스 외부 의존성에 인터페이스를 사용하는 이유는 무엇인가?

  • 진정한 추상화를 위해서가 아닌 목을 사용하기 위함이다.
  • 인터페이스가 없으면 테스트 대역을 만들 수 없으므로 테스트 대상 시스템과 프로세스 외부 의존성 간의 상호 작용을 확인할 수 없다.
  • 따라서 의존성을 목으로 처리할 필요가 없는 한, 프로세스 외부 의존성에 대한 인터터페이스를 두지 말라.
    • 비관리 의존성만 목으로 처리하면 된다.

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

  • 도메인 클래스에 대해 단일 구현으로 인터페이스를 도입하는 이유도 목으로 처리하기 위한 것뿐이다.
  • 그러나 도메인 클래스 간의 상호 작용을 확인해서는 안 된다. 그렇게 하면 깨지기 쉬운 테스트로 이어지고 ,결국 리팩터링 내성이 떨어지게 된다.

통합 테스트 모범 사례

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

도메인 모델 경계 명시하기

  • 항상 도메인 모델을 코드베이스에서 명시적이고 잘 알려진 위치에 두도록 한다.
  • 도메인 모델에 명시적 경계를 지정하면 코드의 해당 부분을 더 잘 보여주고 더 잘 설명할 수 있다.
  • 이러한 경계는 별도의 어셈블리 또는 네임스페이스 형태를 취할 수 있다.(Java의 경우 패키지)

계층 수 줄이기

  • 애플리케이션에 추상 계층이 너무 많으면 코드베이스를 탐색하기 어렵고 아주 간단한 연산이라 해도 숨은 로직을 이해하기 너무 어려워진다.
  • 추상화가 지나치게 많으면 단위 테스트와 통합 테스트에도 도움이 되지 않는다.
    • 간접 계층이 많은 코드베이스는 컨트롤러와 도메인 모델 사이에 명확한 경계가 없는 편이다.
    • 간접 계층이 많으면 각 계층을 따로 검증하는 경향이 훨씬 강해진다. 각 테스트는 특정 계층의 코드만 실행하고 하위 계층은 목으로 처리한다.
    • 최종 결과는 항상 똑같이 낮은 리팩터링 내성과 불충분한 회귀 방지다.
  • 가능한 한 간접 계층을 적게 사용하라.
    • 대부분의 백엔드 시스템에서는 도메인 모델, 애플리케이션 계층, 인프라 계층, 이 세 가지만 활용하면 된다.
    • 인프라 계층은 보통 도메인 모델에 속하지 않는 알고리즘과 프로세스 외부 의존성에 접근할 수 잇는 코드로 구성된다.

순환 의존성 제거하기

  • 순환 의존성은 코드를 읽고 이해하려고 할 때 알아야 할 것이 많아서 큰 부담이 된다.
    • 순환 의존성이 있으면 해결책을 찾기 위한 출발점이 명확하지 않기 때문이다.
  • 순환 의존성은 테스트를 방해한다.
    • 클래스 그래프를 나눠서 동작 단위를 하나 분리하려면 인터페이스에 의존해 목으로 처리해야하는 경우가 많다. 이는 도메인 모델을 테스트할 떄 해서는 안 된다.
  • 인터페이스 사용은 순환 의존성의 문제만 가린다.
    • 컴파일 타임에 순환 참조를 제거할 수 있지만, 여전히 런타임에 순환이 있다.
  • 순환 의존성을 처리하는 더 좋은 방법은 순환 의존성을 제거하는 것이다.
    • 제거가 불가능하다면 순환 의존이 발생하는 그래프를 가낭한 한 작게 만들면 손상을 최소화할 수 있다.

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

  • 테스트에서 두 개 이상의 준비나 실행 또는 검증 구절을 두는 것은 code smell에 해당한다.
  • 각 실행을 고유의 테스트로 추출해 테스트를 나누는 것이 좋다.
    • 각 테스트가 단일 동작 단위에 초첨을 맞추면, 테스트를 더 쉽게 이해하고 필요할 떄 수정할 수 있다.
  • 원하는 상태로 만들기 어려운 프로세스 외부 의존성으로 작동하는 테스트는 예외다.
    • 외부 의존성이 너무 느리거나 호출 수 제한이 있다면, 여러 동작을 하나의 테스트로 묶어서 문제가 있는 프로세스 외부 의존성에 대한 상호 작용 횟수를 줄이는 것이 유리하다.

로깅 기능을 테스트하는 방법

로깅을 테스트해야 하는가?

  • 로깅은 텍스트 파일이나 데이터베이스와 같은 프로세스 외부 의존성에 사이드 이펙트를 초래한다.
    • 이러한 사이드 이펙트를 고객이나 애플리케이션의 클라이언트 또는 개발자 이외의 다른 사람이 보는 경우라면, 로깅은 식별할 수 있는 동작이므로 반드시 테스트해야 한다.
    • 하지만 보는 이가 개발자뿐이라면, 아무도 모르게 자유로이 수정할 수 있는 구현세부 사항이므로 테스트해서는 안 된다.
  • 스티브 프리먼과 냇 프라이스의 ‘Growing Object-Oriented Software, Guided by Tests’에서는 다음과 같이 두 가지 유형의 로깅을 나눈다.
    • 지원 로깅: 지원 담당자나 시스템 관리자가 추적할 수 있는 메시지를 생성한다.
    • 진단 로깅: 개발자가 애플리케이션 내부 상황을 파악할 수 있도록 돕는다.

로깅을 어떻게 테스트해야 하는가?

  • ILogger 위에 래퍼 도입하기
    • 로깅 라이브러리에서 제공하는 ILogger 인터페이스를 직접 목으로 처리하지 말라.
    • 비즈니스에 필요한 모든 지원 로깅을 명시적으로 나열하는 래퍼 클래스 DomainLogger 클래스를 만들고 ILogger 대신 해당 클래스와의 상호 작용을 확인하라
    • DomainLogger는 도메인 언어를 사용해 비즈니스에 필요한 특정 로그 항목을 선언하므로 지원 로깅을 더 쉽게이해하고 유지 보수할 수 있다.
    • 이 구현은 구조화된 로깅 개념과 매우 유사하므로, 로그 파일의 후처리와 분석에서 유연성이 크게 향상된다.
  • 구조화된 로깅 이해하기
    • 구조화된 로깅: 로그 데이터 캡쳐와 렌더링을 분리하는 로깅 기술
    • logger.Info("User Id is " + 12); 와 같이 먼저 문자열을 만든 다음 로그 저장소에 문자열을 기록하는 방식의 문제점은 구조상 결과 로그 파일을 분석하기 어렵다는 점이다.
      • 특정 유형의 메시지가 몇 개인지
      • 특정 사용자 ID와 관련된 메시지가 몇 개인지
    • logger.Info("User Id is {userId}", 12);와 같이 구조화된 로깅 라이브러리는 기저 동작이 크게 다르다.
      • 메시지 템플릿의 해시를 계산하고 해당 해시를 입력 매개변수와 결합해 캡처한 데이터 세트를 형성한다.
      • 데이터의 렌더링 시에는 평범한 로그 파일 뿐만 아니라 JSON, CSV 파일로 렌더링 하도록 로깅 라이브러리를 설정할 수 있어, 분석이 더 쉬워질 수 있다.
  • 지원 로깅고 진단 로깅을 위한 테스트 작성
    • DomainLogger는 프로세스 외부 의존성이므로 도메인 모델에서 호출하면, 비즈니스 로직과 프로세스 외부 의존성과의 통신 간에 분리해야 하는 원칙을 위반한다.
    • 이를 해결하기위해 도메인 이벤트를 도입한다.
    • 단위 테스트에서는 이벤트 발생을 확인하고, 통합 테스트는 목을 써서 DomainLogger와의 상호작용이 올바른지 확인해야 된다.
    • 가능하면 도메인 클래스에서 진단 로깅을 사용하지 말라.

로깅이 얼마나 많으면 충분한가?

  • 지원 로깅은 비즈니스 요구 사항이므로 작성해야 된다.
  • 진단 로깅은 과도하게 사용하지 않는 것이 중요하다.
    • 과도한 로깅은 코드를 혼란스럽게 한다. 특히 도메인 모델에 해당한다.
    • 로그가 많을수록 관련 정보를 찾기가 어려워진다. 신호를 최대한으로 늘리고 잡음을 최소한으로 줄여라.
  • 무언가를 디버깅해야 할 때만 일시적으로 진단 로깅을 사용하라. 디버깅이 끝나면 제거하라.

로거 인스턴스를 어떻게 전달하는가?

  • 앰비언트 컨텍스트: 정적 메서드를 이용한 주입. 단점이 많음.
    • 의존성이 숨어있고 변경하기 어렵다.
    • 테스트가 더 어려워진다.
    • 코드의 잠재적인 문제점을 가릴 수 있다.
  • 메서드나 생성자 주입을 사용하는 것이 좋다.