Why-Integration-Testing
1 통합 테스트를 하는 이유
- 단위 테스트가 비즈니스 로직을 확인하는 데 좋지만 비즈니스 로직을 외부와 단절된 상태로 확인하는 것으로 충분하지 않다
- 각 부분이 데이터베이스나 메시지 버스 등의 외부 시스템과 어떻게 통합되는지 확인해야 한다
2 통합 테스트는 무엇인가?
- 통합 테스트가 무엇인지 알기 위해선 먼저 단위 테스트가 무엇인지 알아야한다
2.1 단위 테스트란?
- 단위 테스트는 다음 세 가지 요구 사항을 충족하는 테스트다
- 단일 동작 단위를 검증하고
- 빠르게 수행하고
- 다른 테스트와 별도로 처리한다
2.2 통합 테스트란?
- 단위 테스트의 3 가지 요구 사항 중 하나라도 충족하지 못하는 테스트는 통합 테스트 범주에 속한다
- 따라서 단위 테스트가 아닌 모든 테스트가 통합 테스트에 해당된다
- 실제로 통합 테스트는 시스템이 프로세스 외부 의존성과 통합해 어떻게 동작하는지 검증한다
단위 테스트와 통합 테스트 비교
- 단위 테스트는 도메인 모델을 다루는 반면, 통합 테스트는 프로세스 외부 의존성과 도메인 모델을 연결하는 코드를 확인한다
2.3 테스트 피라미드
- 단위 테스트와 통합 테스트 간의 균형을 유지하는 것이 중요하다
- 균형을 유지한다는 것은 단위 테스트의 개수와 통합 테스트 개수의 균형을 맞추는 것
2.3.1 통합 테스트의 장단점
통합 테스트의 장점
- 코드를 더 많이 실행시켜 회귀 방지가 단위 테스트보다 우수하다
- 애플리케이션 코드, 라이브러리 코드를 모두 포함하기 때문
- 제품 코드와의 결합도가 낮아 리팩터링 내성도 우수하다
통합 테스트의 단점
- 통합 테스트가 외부 의존성에 직접 작동하며 느려지며, 이러한 테스트는 유지비가 많이 든다
- 유지비 증가의 이유
- 프로세스 외부 의존성 운영이 필요
- 관련된 협력자가 많아 테스트가 비대해짐
2.3.2 단위 테스트의 장단점
단위 테스트의 장점
- 빠른 피드백이 가능하며 유지비가 적게 든다
단위 테스트의 단점
- 통합 테스트와 비교해 회귀 방지와 리팩터링 내성이 떨어진다
2.3.3 테스트 비율
- 단위 테스트와 통합 테스트의 비율은 프로젝트의 특성에 따라 다를 수 있지만 일반적으로 아래와 같다
- 단위 테스트로 가능한 많은 비즈니스 시나리오의 예외 상황을 확인한다
- 통합 테스트는 주요 흐름과 단위 테스트가 다루지 못하는 기타 예외 상황을 다룬다
- 주요 흐름은 시나리오의 성공적인 실행을 의미한다
- 예외 사항은 비즈니스 시나리오 수행 중 오류가 발생하는 경우다
- 대부분을 단위 테스트로 전환하면 유지비를 절감할 수 있다
- 중요한 통합 테스트가 비즈니스 시나리오당 하나 또는 두 개 있으면 시스템 전체의 정확도를 보장할 수 있다
- 이 지침을 따르면 테스트 피라미드의 형태가 나타날 것이다
- Good-Unit-Test.md - 테스트 피라미드 참고
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 테스트에서 다중 실행 구절 사용
- 테스트에서 두 개 이상의 준비나 실행 또는 검증 구절을 두는 것은 코드 악취에 해당한다
- The-Anatomy-Of-A-Unit-Test.md - 실행 구절이 두 줄 이상인 경우 참고
- 테스트가 여러 가지 동작 단위를 한번에 확이해 유지 보수성을 저해한다는 신호다
- 이런 경우 각 실행을 고유의 테스트로 추출해 테스트를 나누는 것이 좋다
예외 사항
- 외부 의존성을 관리하기 어려운 경우 둘 이상의 실행구절을 작성할 수 있다
- 예를 들어 사용자를 등록하면 외부 은행 시스템에서 은행 계좌가 만들어진다고 하자
- 은행에서 샌드박스를 제공하기에 엔드 투 엔드 테스트에서 이 샌드박스를 사용한다
- 문제는 샌드박스가 너무 느리거나 호출 수를 제한한다는 것
- 이러한 시나리오에서 여러 동작을 하나의 테스트로 묶을 수 있다
- 단 단위 테스트는 프로세스 외부 의존성으로 작동하지 않기 때문에 절대로 실행 구절이 여러 개 있어서는 안 된다
- 통합 테스트조차 실행을 여러 단계로 하는 경우는 드물다
- 실제로 다단계 테스트는 거의 엔드 투 엔드 테스트 범주에 속한다