9장, 목 처리에 대한 모범 사례

2022.04.27

9.1 목의 가치를 극대화하기

비관리 의존성에만 목을 사용하게끔 제한하는 것이 중요하지만, 이는 목의 가치를 극대화하기 위한 첫번째 단계일 뿐이다.

9.1.1 시스템 끝에서 상호 작용 검증하기

목을 사용할 때 항상 다음 지침을 따르자. 세스템 끝에서 비관리 의존성과의 상호 작용을 검증하라.

비관릐 의존성과 통신하는 마지막 타입을 목으로 처리하면 통합 테스트가 거치는 클래스의 수가 증가하므로 보호가 향상된다. 이 지침은 EventDispatcher를 목으로 처리하고 싶지 않은 이유이기도 하다. 시스테템의 끝에서부터의 거리가 IMessageBus에 비해 더 멀다.


IBus는 시스템 끝에 있다. IMessageBus는 컨트롤러와 메시지 버스 사이의 타입 사슬에서 중간 고리일 뿐이다. MessageBus 대신 IBus를 목으로 처리하면 회귀 방지가 좋아진다.

외부 시스템은 애플리케이션으로부터 텍스트 메시지를 수신하고, MessageBus와 같은 클래스를 호출하지 않는다. 실제로 텍스트 메시지는 외부에서 식별할 수 있는 유일한 부작용이다. 이러한 메시지를 생성하는 데 참여하는 클래스는 단지 구현 세부 사항일 뿐이다. 따라서 시스템 끝에서 상호 작용을 확인하면 회귀 방지가 좋아질뿐만 아니라 리팩터링 내성도 향상된다.
또한, 이러한 이러한 코드는 코드베이스와의 결합도가 낮기 때문에 낮은 수준의 리팩터링에도 영향을 많이 받지 않는다.

9.1.2 목을 스파이로 대체하기

스파이는 목과 같은 목적을 수행하는 테스트 대역이다. 스파이는 수동으로 작성하는 반면에 목은 목 프레임워크의 도움을 받아 생성한다는 것이 유일한 차이점이다.

[Fact]
public void Changing_email_from_corporate_to_non_corporate()
{
  var busSpy = new BusSpy();
  var messageBus = new MessageBus(busSpy);
  var loggerMock = new Mock<IDomainLogger>();
  var sut = new UserController(db, messageBus, loggerMock.Object);

  busSpy.ShouldlSendNumberOfMessages(1)
    .WithEmailChangedMessage(user.UserId, "new@gmail.com");
}

BusSpy가 제공하는 플루언트 인터페이스(fluent interface)덕분에 이제 메시지 버스와의 상호 작용을 검증하는 것이 간결해졌고 표현력도 생겼다.

9.2 목 처리에 대한 모범 사례

9.2.1 목은 통합 테스트만을 위한 것

목이 통합 테스트만을 위한 것이며 단위 테스트에서 목을 사용하면 안 된다는 지침은 기본 원칙인 비지니스 로직과 오케스트레이션의 분리에서 비롯된다.
코드가 복잡하거나 프로세스 외부 의존성과 통신할 수 있지만, 둘 다는 아니다.

도메인 모델에 대한 테스트는 단위 테스트 범주에 속하며, 컨트롤러는 다루는 테스트는 통합 테스트다. 목은 비관리 의존성에만 해당하며 컨트롤러만 이러한 의존성을 처리하는 코드이기 때문에 통합 테스트에서 컨트롤러를 테스트할 때만 목을 적용해야 한다.

9.2.2 테스트당 목이 하나일 필요는 없음

동작 단위를 구현하는 데 필요한 코드의 양은 관계가 없다. 단일 클래스부터 여러 클래스에 이르기까지 다양하게 걸쳐 있을 수 있고, 아주 작은 메서드에 불과할 수도 있다.

목을 사용해도 같은 원칙이 적용된다. 동작 단위를 검증하는 데 필요한 목의 수는 관계가 없다.

9.2.3 호출 횟수 검증하기

비관리 의존성과의 통신에 관해서는 다음 두 가지 모두 확인하는 것이 중요하다.

  • 예상하는 호출이 있는가?
  • 예상치 못한 호출은 없는가?

이 요구 사항은 다시 비관리 의존성과 하위 호환성을 지켜야 하는데서 비롯된다. 호환성은 양방향이어야 한다. 즉, 애플리케이션은 외부 시스템이 예상하는 메시지를 생략해서는 안 되며 예상치 못한 메시지도 생성해서는 안 된다.

messageBusMock.Verfiy(x => x.SendEmailChangeMessage(user.UserId, "new@gmail.com"));
// 메시지를 전송하는지 확인하는 것만으로 충분하지 않다.

// 아래와 같이 메시지가 정확히 한 번만 전송되는지 확인해야 한다.
messageBusMock.Verfiy(x => x.SendEmailChangeMessage(user.UserId, "new@gmail.com"), Times.Once);  // <- 해당 메서드를 한 번만 호출하는지 확인

9.2.4 보유 타입만 목으로 처리하기

서드파티 라이브러리 위에 항사 어댑터를 작성하고 기본 타입 대신 해당 어댑터를 목으로 처리해야 한다. 관련된 몇 가지 주장을 소개하면 다음과 같다.

  • 서드파티 코드의 작동 방식에 대해 깊이 이해하지 못하는 경우가 많다.
  • 해당 코드가 이미 내장 인터페이스를 제공하더라도 목으로 처리한 동작이 실제로 외부 라이브러리와 일치하는지 확인해야 하므로, 해당 인터페이스를 목으로 처리하는 것은 위험하다.
  • 서드파티 코드의 기술 세부 사항까지는 꼭 필요하지 않기에 어댑터는 이를 추상화하고, 애플리케이션 관점에서 라이브러리와의 관계를 정의한다.

실제로 어댑터는 코드와 외부 환경 사이의 손상 방지 계층으로 작동한다. 어댑터를 통해

  • 기본 라이브러리의 복잡성을 추상화하고
  • 라이브러리에서 필요한 기능만 노출하며
  • 프로젝트 도메인 언어를 사용해 수행할 수 있다.

또한 추상 계층을 두면 이러한 파급 효과를 하나의 클래스(어댑터 등)으로 제한할 수 있다.
보유 타입을 목으로 처리하라.라는 지침은 프로세스 내부 의존성에 적용되지 않는다. 앞서 설명한 것처럼 목은 비관리 의존성에만 해당한다. 따라서 인메모리 의존성이나 관리 의존성을 추상화할 필요가 없다.

요약

  • 시스템 끝에서 비관리 의존성과의 상호 작용을 검증하라. 컨트롤러와 비관리 의존성 사이의 타입 사슬에서 마지막 고리를 목으로 처리하라.
  • 스파이는 직접 작성한 목이다. 시스템 끝에 있는 클래스에 대해서는 스파이가 목보다 낫다. 검증 단계에서 코드를 재사용해 테스트 크기가 줄고 가독성애 개선된다.
  • 검증문을 작성할 때 제품 코드에 의존하지 말라. 테스트에서 별도의 리터럴과 상수 집합을 사용하라. 테스트는 제품 코드와 독립적으로 검사점을 제공해야 한다.
  • 모든 비관릐 의존성에 하위 호환성이 동일한 수준으로 필요한 것은 아니다. 메시지의 정확한 구조가 중요하지 않고 메시지의 존재 여부와 전달하는 정보만 검증하면 시스템의 끝에서 비관리 의존성과의 상호 작용을 검증하라는 지침을 무시할 수 있다. 대표적 예로 로깅이다.
  • 목은 비관리 의존성만을 위한 것이고 이러한 의존성을 처리하는 코드는 컨트롤러뿐이므로 통합 테스트에서 컨트롤러를 테스트할 때만 목을 적용해야 한다. 단위 테스트에서는 목을 사용하지 말라.
  • 테스트에서 사용된 목의 수는 관계가 없다. 목의 수는 비관리 의존성의 수에 따라 달라진다.
  • 목에 예상되는 호출이 있는지와 예상치 못한 호출이 없는지를 확인하라.
  • 보유 타입만 목으로 처리하라. 비관리 의존성에 접근하는 서드파티 라이브러리 위에 어댑터를 작성하라.