단위 테스트에만 전적으로 의존하면 시스템이 전체적으로 잘 작동하는지 확신할 수 없다.
- 단위 테스트는 비즈니스 로직을 확인하는 데 좋다.
📖 8.1 통합 테스트는 무엇인가?
통합 테스트는 테스트 스위트에서 중요한 역할을 하며, 단위 테스트 개수와 통합 테스트 개수의 균형을 맞추는 것도 중요하다.
🔖 8.1.1 통합 테스트의 역할
단위 테스트의 요구사항을 하나라도 충족하지 못하는 테스트는 통합 테스트 범주에 속한다.
- 단일 동작 단위를 검증
- 빠르게 수행
- 다른 테스트와 별도로 처리
실제로 통합 테스트는 대부분 시스템이 외부 의존성과 통합해 어떻게 작동하는지를 검증
- 단위 테스트는 도메인 모델을 다룸
- 통합 테스트는 프로세스 외부 의존성과 도메인 모델을 연결하는 코드를 확인
🔖 8.1.2 다시 보는 테스트 피라미드
단위 테스트와 통합 테스트의 비율은 프로젝트의 특성에 따라 다를 수 있지만, 일반적인 경험에 비춰본 규칙은 다음과 같다.
- 단위 테스트로 가능한 한 많이 비즤스 시나리오의 예외 상황을 확인
- 통합 테스트는 주요 흐름과 단위 테스트가 다루지 못하는 기타 예외 상황을 다룸
- 대부분을 단위 테스트로 전환하면 유지비 절감
- 중요한 통합 테스트가 비즈니스 시나리오당 하나 또는 두개가 있으면 시스템 전체의 정확도를 보장
🔖 8.1.3 통합 테스트와 빠른 실패
빠른 실패 원칙
예기치 않은 오류가 발생하자마자 현재 연산을 중단하는 것
- 피드백 루프 단축
- 지속성 상태 보호
📖 8.2 어떤 프로세스 외부 의존성을 직접 테스트해야 하는가?
🔖 8.2.1 프로세스 외부 의존성의 두 가지 유형
- 관리 의존성
- 전체를 제어할 수 있는 프로세스 외부 의존성
- 데이터베이스
- 비관리 의존성
- 전체를 제어할 수 없는 프로세스 외부 의존성
- SMTP서버, 메시지 버스
관리 의존성은 실제 인스턴스를 사용하고, 비관리 의존성은 목으로 대체
🔖 8.2.2 관리 의존성이면서 비관리 의존성인 프로세스 외부 의존성 다루기
예를 들어, 일부 테이블만 여러 시스템에 접근 권한을 공유한 경우 데이터베이스는 관리 의존성이면서 비관리 의존성이다.
- 일부 테이블만 비관리 의존성으로 취급
- 목으로 테스트 대체
- 나머지 테이블을 관리 의존성으로 처리하고, 상호작용을 검증하지 말고 최종상태를 확인
🔖 8.2.3 통합 테스트에서 실제 데이터베이스를 사용할 수 없으면 어떻게 할까?
이런 경우에는 데이터베이스를 목으로 처리해야 된다고 생각하기 쉽지만 그렇지 않다.
- 통합 테스트의 리팩터링 내성이 저하되기 때문
- 회귀방지도 저하
결국 통합 테스트가 검증할 수 있는 것이 거의 없다. 따라서, 통합 테스트를 아예 작성하지 말고 도메인 모델의 단위 테스트에만 집중하라.
📖 8.3 통합테스트: 예제
7장의 CRM 시스템에 데이터베이스에서 사용자와 회사를 검색하고 의사 결정을 도메인 모델에 위임한 다음, 결과를 데이터베이스에 다시 저장하고 필요한 경우 메시지 버스에 메시지를 싣는다.
🔖 8.3.1 어떤 시나리오를 테스트할까?
public void changing_email_from_corporate_to_non_corporate()
🔖 8.3.2 데이터베이스와 메시지 버스 분류하기
통합테스트는
- 데이터베이스에 사용자와 회사를 삽입
- 해당 데이터베이스에서 이메일 변경 시나리오를 실행
- 데이터베이스 상태를 검증
🔖 8.3.3 엔드 투 엔드 테스트는 어떤가?
통합 테스트 보호 수준이 E2E테스트와 비슷해지면 생략이 가능하고, 프로젝트의 상태 점검을 위해 E2E 테스트를 작성할 수 있다.
- E2E는 관리 의존성을 직접 확인해서는 안 되고, 애플리케이션을 통해 간접적으로 확인해야 한다.
📖 8.4 의존성 추상화를 위한 인터페이스 사용
🔖 8.4.1 인터페이스의 느슨한 결합
인터페이스에 구현이 하나만 있는 경우가 많은데 관습적으로 많이 쓴다. 그 이유는
- 프로세스 외부 의존성을 추상화해 느슨한 결합을 달성하고,
- 기존 코드를 변경하지 않고 새로운 기능을 추가해 OCP을 지키기 때문
물론 모두 오해다. 진정으로 추상화되려면 구현이 두 가지는 있어야 하고, YAGNI 원칙도 위반한다.
- 현재 필요하지 않은 기능에 시간을 들이지 말라
🔖 8.4.2 프로세스 외부 의존성에 인터페이스를 사용하는 이유는 무엇인가?
목을 사용하기 위함이다.
- 목으로 처리할 필요가 없다면 프로세스 외부 의존성에 대한 인터페이스를 두지 말라.
- 진정한 추상화(구현이 둘 이상)는 목과 상관없이 인터페이스로 나타낼 수 있다.
🔖 8.4.3 프로세스 내부 의존성을 위한 인터페이스 사용
프로세스 외부 의존성뿐만 아니라 프로세스 내부 의존성도 인터페이스 기반인 코드를 볼 수 있다.
- 구현이 하나만 있다면 좋지 않다.
- 깨지기 쉬운 테스트와 리팩터링 내성이 떨어지게 된다.
📖 8.5 통합 테스트 모범 사례
- 도메인 모델 경계 명시하기
- 애플리케이션 내 계층 줄이기
- 순환 의존성 제거하기
🔖 8.5.1 도메인 모델 경계 명시하기
항상 도메인 모델을 코드베이스에서 명시적이고 잘 알려진 위치에 두도록 하라.
- 도메인 모델에 명시적 경계를 지정하면 코드의 해당 부분을 더 잘 보여주고 더 잘 설명할 수 있다.
- 이러한 경계는 별도의 어셈블리 또는 네임스페이스 형태를 취할 수 있다.
🔖 8.5.2 계층 수 줄이기
애플리케이션에 추상 계층이 너무 많으면 코드베이스를 탐색하기 어렵고 아주 간단한 연산이라 해도 숨은 로직을 이해하기가 어려워진다.
- 간접 계층은 코드를 추론하는 데 부정적인 영향을 미친다.
- 가능한 한 간접 계층을 적게 사용하라.
- 대부분의 백엔드 시스템에서는 도메인 모델, 애플리케이션 서비스 계층(컨트롤러), 인프라 계층만 활용하면 된다.
🔖 8.5.3 순환 의존성 제거하기
순환 의존성은 둘 이상의 클래스가 제대로 작동하고자 직간접적으로 서로 의존하는 것을 말한다.
대표적으로 콜백(callback)이 있다.
- 순환 의존성은 코드를 읽고 이해하려고 할 때 알아야 할 것이 많아서 큰 부담이 된다.
- 해결책을 찾기 위한 출발점이 명확하지 않기 때문
- 순환의존성은 테스트를 방해한다.
- 인터페이스 사용은 순환 의존성의 문제만 가린다.
- 컴파일 타임에서만 제거 가능하고, 런타임에는 순환참조가 있다.
🔖 8.5.4 테스트에서 다중 실행 구절 사용
테스트에서 두 개 이상의 준비나 실행 또는 검증 구절을 두는 것은 code smell에 해당한다.
- 테스트가 여러 가지 동작 단위를 확인해서 테스트의 유지 보수성을 저해한다는 신호
예를 들어, 하나의 통합 테스트에서 두가지 유스케이스를 모두 확인하려고 하면
준비 -> 실행 -> 검증 -> 실행 -> 검증
의 구조를 가진다.
- 사용자의 상태가 자연스럽게 흐르기 때문에 설득력이 있고, 첫 번째 실행은 두 번째 실행의 준비 단계 역햘을 할 수 있다.
- 이러한 테스트가 초점을 잃고 순식간에 너무 커질 수 있다
- 각 실행을 고유의 테스트로 추출해 테스트를 나누는 것이 장기적으로 유리하다.
- 예외: 원하는 상태로 만들기 어려운 프로세스 외부 의존성으로 작동하는 테스트는 하나로 묶어서 상호 작용 횟수를 줄이는 것이 유리함.
📖 8.6 로깅 기능을 테스트하는 방법
- 로깅을 조금이라도 테스트해야 하는가?
- 만약 그렇다면 어떻게 테스트해야 하는가?
- 로깅이 얼마나 많으면 충분한가?
- 로거 인스턴스를 어떻게 전달할까?
🔖 8.6.1 로깅을 테스트해야 하는가?
- 로깅은 횡단 기능이다.
- 로깅이 개발자 이외의 다른 사람이 보는 경우라면, 로깅은 식별할 수 있는 동작이므로 반드시 테스트해야 한다.
- 로깅이 개발자만 본다면, 구현 세부 사항이므로 테스트해서는 안 된다.
- 지원 로깅: 지원 담당자나 시스템 관리자가 추적할 수 있는 메시지를 생성
- 진단 로깅: 개발자가 애플리케이션 내부 상황을 파악할 수 있도록 돕는다.
🔖 8.6.2 로깅을 어떻게 테스트해야 하는가?
로깅은 프로세스 외부 의존성이 있기 때문에 테스트에 관한 한 프로세스 외부 의존성에 영향을 주는 다른 기능과 동일한 규칙이 적용된다.
- 애플리케이션과 로그 저장소 간의 상호 작용을 검증하려면 목을 써야 한다.
구조화된 로깅은 로그 데이터 캡처와 렌더링을 분리하는 로깅 기술이다.
- 전통적인 로깅
- 간단한 테스트로 작동
- 먼저 문자열을 만든 다음 로그 저장소에 해당 문자열을 기록
- 로그 파일을 분석하기 어려움
log.info("user id is " + 12");
- 구조화된 로깅
- 로그 저장소에 구조가 있음.
- 메시지 템플릿의 해시(공간 효율성을 위해 메시지를 색인 저장소에 저장)를 계산하고 해당 해시를 입력 매개변수와 결합해 캡처한 데이터 세트를 형성
- 캡처한 데이터를 Json, csv 파일로 렌더링하도록 로깅 라이브러리를 설정할 수 있음
log.info("user id is {} ", 12");
🔖 8.6.3 로깅이 얼마나 많으면 충분한가?
진단 로깅을 과도하게 사용하지 않는 것이 중요
- 과도한 로깅은 코드를 혼란스럽게 한다.
- 신호를 최대한으로 늘리고 잡음을 최소한으로 줄여라.
- 도메인 모델에서는 진단 로깅을 절대 사용하지 않도록 하라.
🔖 8.6.4 로거 인스턴스를 어떻게 전달하는가?
정적 메서드를 사용하는 방법이 있음
- ambient context라고도 함.
- 의존성이 숨어있고 변경하기가 어렵다.
- 테스트가 더 어려워진다.
- 코드의 잠재적인 문제를 가린다.
클래스 생성자를 통해 명시적으로 주입하는 방법이 있음
📖 8.7 결론
식별할 수 있는 동작인지, 구현 세부 사항인지 구분하여 프로세스 외부 의존성과의 통신을 살펴보자.
- 개발자가 아닌 사람이 로그를 볼 수 있으면 로깅 기능을 목으로 처리
- 개발자만 로그를 본다면 테스트를 하지 말라