- 단위 테스트에만 전적으로 의존하면 시스템이 전체적으로 잘 작동하는지 확신할 수 없다.
통합 테스트는 무엇인가?#
통합 테스트의 역할#
- 아래 단위 테스트 세 가지 요구 사항중 하나라도 충족하지 못하면 통합 테스트의 범주에 속한다.
- 단일 동작 단위를 검증하고,
- 빠르게 수행하고,
- 다른 테스트와 별도로 처리한다.
- 단위 테스트는 도메인 모델을 다루는 반면, 통합 테스트는 프로세스 외부 의존성과 도메인 모델을 연결하는 코드(컨트롤러)를 확인한다.
다시 보는 테스트 피라미드#
- 통합 테스트는 유지비가 많이 든다.
- 프로세스 외부 의존성 운영이 필요함
- 관련된 협력자가 많아서 테스트가 비대해짐
- 단위 테스트로 가능한 한 많이 비즈니스 시나리오의 예외 상황을 확인하고, 통합 테스트는 주요 흐름(happy path)과 단위 테스트가 다루지 못하는 기타 예외 상황을 다룬다.
- 대부분을 단위 테스트로 전환하면 유지비를 절감할 수 있고, 단위 테스트와 통합 테스트 사이의 피라미드 같은 비율을 만든다.
- 복잡도가 낮은 단순 애플리케이션은 도메인 모델과 알고리즘 사분면에 코드가 거의 없어, 피라미드 대신 작사각형 모양이 된다.
통합 테스트와 빠른 실패#
- 통합 테스트에서 프로세스 외부 의존성과 상호 작용을 모두 확인하려면 가장 긴 주요 흐름을 선택하라.
- 이렇게 모든 상호 작용을 거치는 흐름이 없으면, 외부 시스템과의 통신을 모두 확인하는 데 필요한 만큼 통합 테스트를 추가로 작성하라.
- 어떤 예외 상황에 잘못 실행돼 전체 애플리케이션이 즉시 실패하면 해당 예외 상황은 테스트할 필요가 없다.
- 좋지 않은 테스트를 작성하는 것보다는 테스트를 작성하지 않는 것이 좋다.
- 빠른 실패 원칙: 예기치 않는 오류가 발생하자마자 현재 연산을 중단하는 것을 의미한다. 이 원칙은 다음을 통해 애플리케이션의 안정성을 높인다.
- 피드백 루프 단축: 버그를 빨리 발견할수록 더 쉽게 해결할 수 있다. 이미 운영 환경으로 넘어온 버그는 개발 중에 발견된 버그보다 수정 비용이 훨씬 더 크다.
- 지속성 상태 보호: 버그는 애플리케이션 상태를 손상시킨다. 손상된 상태가 데이터베이스로 침투하면, 고치기가 훨씬 어려워진다. 빨리 실패하면 손상이 확산되는 것을 막을 수 있다.
어떤 프로세스 외부 의존성을 직접 테스트해야 하는가?#
- 외부 의존성 검증 구현 방식 두 가지
- 실제 프로세스 외부 의존성을 사용한다.
- 목으로 대체한다.
프로세스 외부 의존성의 두 가지 유형#
- 프로세스 외부 의존성은 두 가지 범주로 나뉜다.
- 관리 의존성(전체를 제어할 수 있는 프로세스 외부 의존성): 애플리케이션을 통해서만 접근할 수 있으며, 해당 의존성과의 상호 작용은 외부 환경에서 볼 수 없다.
- 비관리 의존성(전체를 제어할 수 없는 프로세스 외부 의존성): 해당 의존성과의 상호 작용을 외부에서 볼 수 있다.
- 관리 의존성은 실제 인스턴스를 사용하고, 비관리 의존성은 목으로 대체한다.
- 관리 의존성과의 통신은 구현 세부 사항이다.
- 비관리 의존성과의 통신은 식별할 수 있는 동작이다.
관리 의존성이면서 비관리 의존성인 프로세스 외부 의존성 다루기#
- 관리 의존성과 비관리 의존성 모두의 속성을 나타내는 프로세스 외부 의존성의 예: 다른 애플리케이션이 접근할 수 있는 데이터베이스
- 이러한 경우는 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와의 상호작용이 올바른지 확인해야 된다. - 가능하면 도메인 클래스에서 진단 로깅을 사용하지 말라.
로깅이 얼마나 많으면 충분한가?#
- 지원 로깅은 비즈니스 요구 사항이므로 작성해야 된다.
- 진단 로깅은 과도하게 사용하지 않는 것이 중요하다.
- 과도한 로깅은 코드를 혼란스럽게 한다. 특히 도메인 모델에 해당한다.
- 로그가 많을수록 관련 정보를 찾기가 어려워진다. 신호를 최대한으로 늘리고 잡음을 최소한으로 줄여라.
- 무언가를 디버깅해야 할 때만 일시적으로 진단 로깅을 사용하라. 디버깅이 끝나면 제거하라.
로거 인스턴스를 어떻게 전달하는가?#
- 앰비언트 컨텍스트: 정적 메서드를 이용한 주입. 단점이 많음.
- 의존성이 숨어있고 변경하기 어렵다.
- 테스트가 더 어려워진다.
- 코드의 잠재적인 문제점을 가릴 수 있다.
- 메서드나 생성자 주입을 사용하는 것이 좋다.
comments powered by