목과 스텁 구분

테스트 대역 유형

  • 목: 외부로 나가는 상호 작용을 _모방_하고 _검사_한다. 이러한 상호 작용은 SUT가 상태를 변경하기 위한(사이드 이펙트가 있는) 의존성을 호출하는 것에 해당한다.
    • 목과 스파이로 분류할 수 있다. 스파이는 수동으로 작성하는 반면, 목은 목 프레임워크의 도움을 받아 생성한다.
  • 스텁: 내부로 들어오는 상호 작용을 _모방_한다. 이러한 상호 작용은 SUT가 입력 데이터를 얻기 위한 의존성을 호출하는 것에 해당한다.
    • 스텁, 더미, 페이크로 분류할 수 있다.
    • 더미: 널값이나 가짜 문자열과 같이 단순하고 하드코딩된 값이다.
    • 스텁: 시나리오마다 다른 값을 반환하게끔 구성할 수 있도록 필요한 것을 다 갖춘 완전한 의존성이다.
    • 페이크: 대다수의 목적은 스텁과 같다. 아직 존재하지 않는 의존성을 대체하고자 구현하는 생성에 대한 차이점이 있다.

도구로서의 목과 테스트 대역으로서의 목

  • 도구로서의 목: 목 라이브러리의 클래스.
  • 도구로서의 목은 목과 스텁 두 가지 유형의 테스트 대역을 생성할 수 있다.

스텁으로 상호 작용을 검증하지 말라

  • 스텁과의 상호 작용을 검증하는 것은 취약한 세트를 야기하는 일반적인 안티 패턴이다.
  • 테스트에서 거짓 양성을 피하고 리팩터링 내성을 향상시키는 방법은 구현 세부 사항이 아니라 최종 결과를 검증하는 것뿐이다.
    • 최종 결과는 이상적으로 비개발자들에게 의미가 있어야한다.
  • 스텁에서 내부로 들어오는 상호 작용은 SUT가 실제 결과를 생성하기 위한 구현 세부 사항이다.
  • 최종 결과가 아닌 사항을 검증하는 관행을 과잉 명세라고 부른다.
  • 또한 목을 쓰는 경우에도 항상 테스트 취약성을 초래하는 것은 아니지만, 대다수는 그렇다.

목과 스텁 함께 쓰기

  • 때로는 목과 스텁의 특성을 모두 나타내는 테스트 대역을 만들 필요가 있다.
  • 테스트 대역은 목이면서 스텁이지만, 여전히 목이라고 부른다. 목이라는 사실이 스텁이라는 사실보다 더 중요하기 때문이다.

목과 스텁은 명령과 조회에 어떻게 관련돼 있는가?

  • 목과 스텁의 개념은 명령 조회 분리(CQS, Command Query Separation) 원칙과 관련이 있다.
  • CQRS는 CQS에서 확장된 개념이다. CQS는 메서드 단위에서 분리하는 반면, CQRS는 객체나 시스템 단위에서 분리하는 점이 다르다.
  • CQS 원칙에 따르면 모든 메서드는 명령이거나 조회여야 하며, 이 둘을 혼용해서는 안된다.
    • 명령은 사이드 이펙트를 일으키고 반환 값이 없다.
    • 조회는 사이드 이페그가 없고 반환 값이 있다.
  • 항상 CQS 원칙을 따를 수있는 것은 아니다. (예: stack.Pop())

식별할 수 있는 동작과 구현 세부 사항

  • 테스트에 거짓 양성이 있는 주요 이유는 코드의 구현 세부 사항과 결합돼 있기 때문이다.
    • 이러한 강결합을 피하는 방법은 코드가 생성한느 최종 결과를 검증하고 구현 세부 사항과 테스트를 가능한 한 떨어뜨리는 것뿐이다.

식별할 수 있는 동작은 공개 API와 다르다

  • 모든 제품 코드는 2차원으로 분류할 수 있다.
    • 공개 API 또는 비공개 API
    • 식별할 수 있는 동작 또는 구현 세부 사항
  • 코드가 시스템의 식별할 수 있는 동작이려면 다음 중 하나를 해야 한다.
    • 클라이언트가 목표를 달성하는 데 도움이 되는 연산을 노출하라. 연산은 계산을 수행하거나 사이드 이펙트를 초래하거나 둘 다 하는 메서드다.
    • 클라이언트가 목표를 달성하는 데 도움이 되는 상태를 노출하라. 상태는 시스템의 현재 상태다.
  • 클라이언트는 동일한 코드베이스, 외부 애플리케이션, 사용자 인터페이스 등의 클라이언트 코드가 있다.
  • 구현 세부 사항은 이 두 가지 중 아무것도 하지 않는다.
  • 이상적으로 시스템의 공개 API는 식별할 수 있는 동작과 일치해야 하며, 모든 구현 세부 사항은 클라이언트 눈에 보이지 않아야 한다.
  • 그러나 종종 시스템의 공개 API가 식별할 수 있는 동작의 범위를 넘어 구현 세부 사항을 노출하기 시작한다. 이러한 시스템의 구현 세부 사항은 공개 API로 유출된다.

구현 세부 사항 유출: 연산의 예

  • 클라이언트가 목표를 달성하는 데 도움이 되는 작업을 노출하라.
  • 클라이언트가 목표를 달성하는 데 도움이 되는 상태를 노출하라.
  • 단일한 목표를 달성하고자 클래스에서 호출해야 하는 연산의 수가 1보다 크면 해당 클래스에서 구현 세부 사항을 유출할 가능성이 있다.

잘 설계된 API와 캡슐화

  • 코드 캡슐화: 구현 세부 사항을 숨기고 데이터와 기능을 결합하는 것이 해당 목표를 달성하기 위한 수단
    • 구현 세부사항을 숨기려면 클라이언트의 시야에서 클래스 내부를 가릴 수 있기 때문에 내부를 손상시킬 위험이 적다.
    • 데이터와 연산을 결합하면 해당 연산이 클래스의 불변성을 위반하지 않도록 할 수 있다.

구현 세부 사항 유출: 상태의 예

  • 모든 구현 세부 사항을 비공개로 하면 테스트가 식별할 수 있는 동작을 검증하는 것 외에는 다른 선택지가 없으며, 이로 인해 리팩터링 내성도 자동으로 좋아진다.

목과 테스트 취약성 간의 관계

육각형 아키텍처 정의

  • 애플리케이션은 도메인과 애플리케이션 서비스라는 두 계층으로 구성된다.
    • 도메인 계층: 애플리케이션의 필수 기능으로 비즈니스 로직이 포함돼 있다.
    • 애플리케이션 서비스 계층: 도메인 계층 위에 있으며 외부 환경과의 통신을 조정한다.
      • DB조회하고 해당 데이터로 도메인 클래스 인스턴스 구체화
      • 해당 인스턴스에 연산 호출
      • 결과를 데이터베이스에 다시 저장
  • 육각 아키텍처의 세 가지 지침
    • 도메인 계층과 애플리케이션 서비스 계층 간의 관심사 분리: 도메인 계층은 애플리케이션 도메인 지식 모음으로, 애플리케이션 서비스 계층은 일련의 비즈니스 유스케이스로 본다.
    • 애플리케이션 내부 통신: 애플리케이션 서비스 계층에서 도메인 계층으로 흐르는 단방향 의존성 흐름을 규정한다. 도메인 계층은 외부 환경에서 완전히 격리돼야 한다.
    • 애플리케이션 간의 통신: 외부 애플리케이션은 애플리케이션 서비스 계층이 있는 공통 인터페이스를 통해 해당 애플리케이션에 연결된다.
  • 각 계층의 API를 잘 설계하면(구현 세부 사항을 숨기면) API와 테스트 모두 프랙탈 구조를 갖는다.
    • 애플리케이션 서비스와 도메인 클래스가 달성하는 목표는 같지만 서로 다른 수준에서 동작을 검증한다.
  • 코드베이스의 공개 API를 항상 비즈니스 요구 사항에 따라 추적하라는 지침은 대부분의 도메인 클래스와 애플리케이션 서비스에 적용되지만, 유틸리티나 인프라 코드에는 적용되지 않는다.
    • 해당 코드는 너무 낮은 수준이고 세밀해서 구체적인 비즈니스 유스케이스로 추적할 수 없다.

시스템 내부 통신과 시스템 간 통신

  • 애플리케이션에는 시스템 내부 통신과 시스템 간 통신이 있다.
    • 시스템 내부 통신: 애플리케이션 내 클래스 간의 통신
    • 시스템 간 통신: 애플리케이션이 다른 애플리케이션과 통신
  • 시스템 내부 통신은 구현 세부 사항이고, 시스템 간 통신은 식별할 수 있는 동작이다.

시스템 내부 통신과 시스템 간 통신의 예

  • 시스템 간 통신은 목으로 하는 이유는 타당하다.
    • 리팩터링 후에도 이러한 통신 유형이 그대로 유지되도록 하기때문에 테스트 취약성을 야기하지 않는다.
  • 시스템 내부 통신은 유스케이스의 중간 단계에 해당한다. 목으로 하면 취약한 테스트가 된다.

단위 테스트의 고전파와 런던파 재고

  • 런던파를 따라 목을 무분별하게 사용하면 종종 구현 세부 사항에 결합돼 테스트에 리팩터링 내성이 없게 된다.
    • 고전파는 테스트 간에 공유하는 의존성만 교체하므로 이 문제에 훨씬 유리하다.

모든 프로세스 외부 의존성을 목으로 해야하는 것은 아니다.

  • 의존성 유형
    • 공유 의존성: 테스트 간에 공유하는 의존성
    • 프로세스 외부 의존성: 프로그램의 실행 프로세스 외에 다른 프로세스를 점유하는 의존성(예를 들어 데이터베이스, 메시지 버스, SMTP 서비스 등)
    • 비공개 의존성: 공유하지 않는 모든 의존성
  • 고전파에서는 공유 의존성을 피할 것을 권고한다.
  • 고전파에서는 모든 외부 의존성을 목으로 해야 하는 것은 아니다.
    • 프로그램 외부 의존성이 애플리케이션을 통해서만 접근할 수 있으면, 이러한 의존성과의 통신은 시스템에서 식별할 수 있는 동작이 아니다.
    • 애플리케이션이 외부 시스템에 대한 프록시 같은 역할을 하고 클라이언트가 직접 접근할 수 없으면, 구현 세부 사항이 된다.
    • 예시: DB.
    • 하지만, 피드백 속도를 저하지키지 않고 외부 의존성을 테스트할 수 있는 방법을 고민해야된다.

의문점: DB는 공유 의존성이 될 수 있기 때문에 목으로 만드는 것이 좋지 않나?