BottleH Blog

Unit Testing - 8장 통합 테스트를 하는 이유

    Tags

  • Test
Unit Testing - 8장 통합 테스트를 하는 이유 thumbnail

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

  • 단위 테스트는 비즈니스 로직을 확인하는 데 좋다.

📖 8.1 통합 테스트는 무엇인가?


통합 테스트는 테스트 스위트에서 중요한 역할을 하며, 단위 테스트 개수와 통합 테스트 개수의 균형을 맞추는 것도 중요하다.

🔖 8.1.1 통합 테스트의 역할

단위 테스트의 요구사항을 하나라도 충족하지 못하는 테스트는 통합 테스트 범주에 속한다.

  • 단일 동작 단위를 검증
  • 빠르게 수행
  • 다른 테스트와 별도로 처리

실제로 통합 테스트는 대부분 시스템이 외부 의존성과 통합해 어떻게 작동하는지를 검증

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

🔖 8.1.2 다시 보는 테스트 피라미드

단위 테스트와 통합 테스트의 비율은 프로젝트의 특성에 따라 다를 수 있지만, 일반적인 경험에 비춰본 규칙은 다음과 같다.

  • 단위 테스트로 가능한 한 많이 비즤스 시나리오의 예외 상황을 확인
  • 통합 테스트는 주요 흐름과 단위 테스트가 다루지 못하는 기타 예외 상황을 다룸
  • 대부분을 단위 테스트로 전환하면 유지비 절감
  • 중요한 통합 테스트가 비즈니스 시나리오당 하나 또는 두개가 있으면 시스템 전체의 정확도를 보장

8-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 통합 테스트 모범 사례


  1. 도메인 모델 경계 명시하기
  2. 애플리케이션 내 계층 줄이기
  3. 순환 의존성 제거하기

🔖 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 결론

식별할 수 있는 동작인지, 구현 세부 사항인지 구분하여 프로세스 외부 의존성과의 통신을 살펴보자.

  • 개발자가 아닌 사람이 로그를 볼 수 있으면 로깅 기능을 목으로 처리
  • 개발자만 로그를 본다면 테스트를 하지 말라
Written by@BottleH
Back-End Developer

GitHub