8장. 통합 테스트를 하는 이유

[단위 테스트] 3. 통합 테스트


8장. 통합 테스트를 하는 이유

개요

단위 테스트는 모두 통과했지만 애플리케이션이 작동하지 않는 상황에 처할 수 있다

소프트웨어 구성 요소를 서로 격리하여 검증하는 것도 중요하지만

구성 요소들이 어떻게 데이터베이스와 같은 외부 시스템과 통합하여 작동하는지 검증하는 것도 중요하다

통합 테스트는 무엇인가

통합 테스트의 역할

  • 단위 테스트는

    • 단일 동작 단위를 검증하고

    • 빠르게 수행하고

    • 다른 테스트와 별도로 처리한다

    라는 세 가지 요구 사항을 충족하는 테스트이다

  • 여기서 하나라도 충족하지 못하는 테스트는 통합 테스트이다

    단위 테스트가 아니면 통합 테스트이다.

  • 통합 테스트는 시스템이 프로세스 외부 의존성과 통합해 어떻게 작동하는지 검증한다

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

  • 코드 사분면에서 컨트롤러 사분면에 속하는 코드를 다룬다

다시 보는 테스트 피라미드

  • 통합 테스트는 프로세스 외부 의존성에 직접 작동하면 느려진다.

    느려지면

    • 프로세스 외부 의존성 운영이 필요하고

    • 관련된 협력자가 많아서 테스트가 커진다

    따라서 유지비가 많이 든다.

    단위 테스트와 통합 테스트 간의 균형을 유지하는 것이 중요하다

  • 통합 테스트는 코드양이 많으니까 회귀 방지가 우수하고

    제품 코드와의 결합도가 낮아서 리팩토링 내성도 우수하다

  • 단위 테스트로 최대한 비즈니스 시나리오의 예외 상황을 확인하고

    통합 테스트로 주요 흐름과 기타 예외 상황을 확인한다.

  • 간단한 프로젝트 일수록 테스트할 도메인 모델과 알고리즘 사분면이 작아 단위 테스트 수가 적지만

    통합 테스트는 다른 시스템과 통합해 어떻게 동작하는지 확인하는 것이므로 간단한 프로젝트에서도 중요하다

통합 테스트와 빠른 실패

통합 테스트를 사용해 비즈니스 시나리오당 하나의 주요 흐름과 단위 테스트로 처리할 수 없는 모든 예외 상황을 다루는 지침

  • 통합 테스트에서 프로세스 외부 의존성과의 상호 작용을 모두 확인하려면 가장 긴 주요 흐름을 선택한다.

    모든 상호 작용을 거치는 흐름이 없으면, 외부 시스템과의 통신을 모두 확인하는데 필요한 만큼 통합 테스트를 추가로 작성한다.

  • 단위 테스트에 다룰 수 없는 예외가 있듯이 통합 테스트도 예외가 있다.

    어떠한 예외 상황에 잘못 실행돼 전체 애플리케이션이 즉시 실패하면 해당 예외 상황은 테스트할 필요가 없다

    통합 테스트에서 대안으로 빠른 실패 원칙이 있다

  • 빠른 실패 원칙

    • 예기치 않은 오류가 발생하자마자 현재 연산을 중단한다.

    • 피드백 루프 단축과 지속성 상태 보호를 통해 안정성을 높인다

      • 피드백 루프 단축 : 버그를 빨리 발견할수록 더 쉽게 해결할 수 있다. 운영 환경으로 넘어가면 해결하기 어려워진다.

      • 지속성 상태 보호 : 버그는 애플리케이션 상태를 손상시킨다. 데이터베이스에 침투하기 전에 실패해서 손상의 확산을 막는다

    • 보통 예외를 던져 현재 연산을 중지한다.

      예외는 프로그램 흐름을 중단하고 실행 스택에서 가장 높은 레벨로 올라간 후 로그를 남기고 작업을 종료하거나 재시작할 수 있다.

      빠른 실패 원칙과 부합한다.

    • 전제 조건은 빠른 실패 원칙의 예다

      전제 조건이 실패하면 애플리케이션 상태에 대해 가정이 잘못된 것을 의미하는데, 이는 버그에 해당한다.

    • 설정 파일에서 데이터를 읽는 것도 빠른 실패 원칙의 예다

      설정 파일의 데이터가 불완전하거나 잘못된 경우 예외가 발생하도록 애플리케이션 시작 부근에 둬서 문제가 있으면

      애플리케이션이 시작하지 않도록 할 수 있다.

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

시스템이 프로세스 외부 의존성과 어떻게 통합하는지를 검증하는 것을 구현하는 방식은 두 가지가 있다

실제 프로세스 외부 의존성을 사용하거나 해당 의존성을 목으로 대체하는 것이다

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

모든 프로세스 외부 의존성은 두 가지 범주로 나뉜다

  • 관리 의존성

    • 전체를 제어할 수 있는 프로세스 외부 의존성

    • 애플리케이션을 통해서만 접근할 수 있다.

    • 해당 의존성과의 상호 작용은 외부에서 볼 수 없다

    • ex) 데이터베이스

    • 외부 시스템은 보통 데이터베이스에 직접 접근하지 않고 애플리케이션에서 제공하는 API를 통해 접근한다.

  • 비관리 의존성

    • 전체를 제어할 수 없는 프로세스 외부 의존성

    • 해당 의존성과의 상호 작용을 외부에서 볼 수 있다

    • ex) SMTP 서버와 메시지 버스

    • 다른 애플리케이션에서 볼 수 있는 부작용을 발생시킨다

  • 관리 의존성과의 통신은 구현 세부 사항이고

    비관리 의존성과의 통신은 시스템의 식별할 수 있는 동작이다

  • 관리 의존성은 실제 인스턴스를 사용하고 비관리 의존성은 목으로 대체한다

  • 하위 호환성을 지키기 위해 비관리 의존성에 대한 통신 패턴을 유지해야 한다.

    목을 사용하면 모든 가능한 리팩토링을 고려해서 통신 패턴 영속성을 보장할 수 있다

  • 관리 의존성과 통신하는 것은 애플리케이션 뿐이므로 하위 호환성을 유지할 필요가 없다

    외부 클라이언트는 데이터베이스를 어떻게 구성하는지 신경 쓰지 않는다

    실제 인스턴스를 사용하여 시스템의 최종 상태를 확인한다

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

  • 다른 애플리케이션이 접근할 수 있는 데이터베이스와 같이 관리 의존성과 비관리 의존성 모두의 속성을 나타내는 프로세스 외부 의존성이 있다

    시스템 간의 통합을 구현하는데 데이터베이스를 사용하는 것보다 API나 메시지 버스를 사용하는 것이 낫긴 하다

  • 다른 애플리케이션이 접근할 수 있는 테이블을 비관리 의존성으로 취급한다.

    테이블을 이용한 통신 패턴이 바뀌지 않도록 목을 사용하여 데이터베이스와의 상호 작용을 검증하지 말고 데이터베이스의 최종 상태를 확인한다.

    공유 테이블과 상호 작용하는 방식을 변경하면 다른 어플리케이션이 어떻게 반응하는지 알 수 없다

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

  • 통합 테스트에서 관리 의존성을 실제 버전으로 사용할 수 없는 경우가 있다

    보안 정책 혹은 테스트 데이터베이스 인스턴스를 설정하고 유지하는 비용이 크기 때문이다

  • 이 때, 관리 의존성을 실제 인스턴스가 아닌 그냥 통합 테스트를 작성하지 말고 도메인 모델의 단위 테스트에 집중한다

    목으로 대체 한다고해도 통합 테스트의 리팩터링 내성이 저하되고 테스트는 회귀 방지도 떨어진다

    이러한 관리 의존성이 유일한 관리 의존성이면 기존 단위 테스트 세트와 같다

통합 테스트 : 예제

어떤 시나리오를 테스트할까?

  • 통합 테스트는 모든 프로세스 외부 의존성을 거치는 가장 긴 주요 흐름과 단위 테스트로는 수행할 수 없는 모든 예외 상황을 다루는 것이다

  • 예제에서 가장 긴 주요 흐름은 기업 이메일에서 일반 이메일로 변경하는 것이다

    데이터베이스에서는 사용자와 회사 모두 업데이트하고 메시지 버스로 메시지도 보낸다

  • 예제에서 이메일을 변경할 수 없는 시나리오라는 단위 테스트로는 테스트하지 않는 한 가지 예외 상황이 있다

    하지만 컨트롤러에서 확인하고 빠른 실패하기 때문에 통합 테스트로 테스트할 필요는 없다

데이터베이스와 메시지 버스 분류하기

  • 예제에서 데이터베이스는 다른 어떤 시스템도 접근할 수 없으므로 관리 의존성이므로 실제 인스턴스를 사용해야 한다.

  • 메시지 버스는 다른 시스템과의 통신이 목적이므로 비관리 의존성이다. 목으로 대체하고 컨트롤러와 목 사이의 상호 작용을 검증한다.

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

  • 엔드 투 엔드 테스틑 외부 클라이언트를 모방하므로 모든 프로세스 외부 의존성을 목으로 대체하지 않는다.

    메시지 버스는 직접 확인하고 관리 의존성인 데이터베이스는 애플리케이션을 통해 간접 검증한다.

    통합 테스트의 보호 수준이 엔드 투 엔트 테스트와 비슷해지므로 생략할 수 있지만

    배포 후 상태 점검을 위해 사용할 수 있다.

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

인터페이스와 느슨한 결합

  • 일반적으로 단일 구현을 위한 인터페이스가

    • 프로세스 외부 의존성을 추상화해 느슨한 결합을 달성하고

    • 기존 코드의 변경 없이 새로운 기능을 추가해 개방 폐쇄 원칙 OCP를 지킨다

    라는 이유로 사용하지만 단일 구현을 위한 인터페이스는 추상화가 아니다

  • 인터페이스가 추상화되려면 구현이 두 개는 있어야 한다

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

  • 그럼에도 불구하고 프로세스 외부 의존성에 인터페이스를 사용하는 이유는 목을 사용하기 위해서 이다.

    따라서 목으로 처리할 필요가 없으면 인터페이스는 없어도 된다

    비관리 의존성만 목으로 처리하므로 비관리 의존성에만 인터페이스를 쓰고 관리 의존성을 컨트롤러에 명시적으로 주입하고 구현으로 사용한다.

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

  • 프로세스 외부 의존성과 마찬가지로 도메인 클래스에 대해 단일 구현으로 인터페이스를 도입하는 이유는

    목으로 처리하기 위한 것 뿐이다

  • 그러나 프로세스 외부 의존성과 달리 도메인 클래스 간의 상호 작용을 확인해서는 안된다

    구현 세부 사항과 결합된 깨지지 쉬운 테스트가 되고 리팩터링 내성이 떨어지기 때문이다

통합 테스트 모범 사례

도메인 모델 경계 명시하기

  • 단위 테스트는 도메인 모델과 알고리즘을 대상으로하고

    통합 테스트는 컨트롤러를 대상으로 하므로

    도메인 클래스와 컨트롤러 사이의 명확한 경계로 단위 테스트와 통합 테스트의 차이점을 쉽게 구별할 수 있다

계층 수 줄이기

  • 간접 계층을 추가해서 코드를 추상화하고 일반화하려 한다.

    간접 계층은 코드를 추론하는 데 방해된다.

  • 추상화가 지나치게 많으면 단위 테스트와 통합 테스트에도 도움이 되지 않는다.

    간접 계층이 많으면 도메인 모델과 컨트롤러 사이의 경계가 명확하지 않다.

  • 각 계층을 따로 검증하려 하므로 통합 테스트의 가치가 떨어지고 하위 계층은 목으로 처리한다.

  • 가능한 간접 계층은 적게 사용한다.

    백엔드 시스템에서는 도메인 모델, 애플리케이션 서비스 계층(컨트롤러), 인프라 계층(도메인 모델에 속하지 않는 알고리즘과 프로그램 외부 의존성에 접근할 수 있는 코드),

    이 세가지만 활용하며 된다.

순환 의존성 제거하기

  • 코드를 읽기 어려워 진다

  • 동작 단위를 하나 분리하려면 목을 사용하게 되는데 도메인 모델을 테스트할 때 해서는 안된다

  • 인터페이스 사용도 순환 의존성을 해결할 수 없다.

  • 인터페이스에 의존하지 않도록 하고, 순환 호출 대신 결과를 리턴하게 한다.

    public class CheckoutService {
        public void checkOut(int orderId) {
            ReportGenerationService reportGenerationService = new ReportGenerationService();
            reportGenerationService.generateReport(orderId, this);
            /* 기타 코드 */
        }
    
        public void callBack() {
            // ...
        }
    }
    
    public class ReportGenerationService {
        public void generateReport(int orderId, CheckoutService checkoutService) {
            // ...
            checkoutService.callBack();
        }
    }
    
    ////////////////////////////////////////
    
    public class CheckoutService {
        public void checkOut(int orderId) {
            ReportGenerationService reportGenerationService = new ReportGenerationService();
            Report report = reportGenerationService.generateReport(orderId, this);
            /* 기타 코드 */
        }
    }
    
    public class ReportGenerationService {
        public Report generateReport(int orderId, CheckoutService checkoutService) {
            // ...
        }
    }

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

  • 테스트에서 두 개 이상의 준비나 실행 또는 검증 구절을 두는 것은 코드 악취다

    테스트가 여러 가지 동작 단위를 검증해서 테스트의 유지 보수성을 저해한다

  • 각각의 실행을 고유의 테스트로 추출해서 나누는 것이 좋다

    단일 동작에 초점을 두면, 이해하기 쉽고 수정하기 쉽다.

  • 예외로, 원하는 상태로 만들기 어려운 프로세스 외부 의존성으로 작동하는 테스트가 있다.

    단위 테스트는 프로세스 외부 의존성으로 작동하지 않기 때문에 실행 구절이 하나만 있어야 한다.

로깅을 테스트 하는 방법

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

  • 로깅은 코드 어느 부분에서나 필요할 수 있다

  • 로깅이 클라이언트 또는 개발 이외의 다른 사람이 보는 경우는 식별할 수 있는 동작이므로 테스트해야 하고

    개발자만 본다면 구현 세부 사항이므로 테스트해서는 안된다

  • 지원 로깅 : 시스템 관리자가 추적할 수 있는 메시지를 생성

    진단 로깅 : 개발자가 애플리케이션 내부 상황을 파악하도록 도움

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

  • 로깅도 프로세스 외부 의존성이 있다.

    따라서 애플리케이션과 로그 저장소 간의 상호 작용을 검증하려면 목을 써야 한다.

  • 지원 로깅은 비즈니스 요구 사항이므로 목으로 처리하고

    진단 로깅은 테스트 하지 않는다

Last updated