원문: https://testing.googleblog.com/2020/07/testing-on-toilet-dont-mock-types-you.html (Translated by Google Gemini)


단위 테스트를 작성할 때, 테스트 대상 코드가 의존하는 타입을 목(mock) 처리하는 것은 일반적인 관행입니다. 하지만, 당신이 소유하지 않은 타입(types you don’t own)을 목 처리하는 것은 거의 항상 피해야 합니다. 여기서 “소유하지 않은 타입"이란 당신의 팀이 작성하거나 관리하지 않는 모든 클래스나 인터페이스를 의미합니다. 여기에는 자바 표준 라이브러리의 URL이나 서드파티 라이브러리의 JSONObject와 같은 타입들이 포함됩니다.

예를 들어, java.net.HttpURLConnection을 직접 목 처리하는 테스트를 생각해 봅시다.

@Test
public void testProcessUrl_connectionFailed() throws Exception {
  HttpURLConnection mockConnection = mock(HttpURLConnection.class);
  when(mockConnection.getResponseCode()).thenThrow(new IOException());

  Processor processor = new Processor();
  assertFalse(processor.processUrl(mockConnection));
}

이 테스트에는 몇 가지 심각한 문제가 있습니다.

  • 불안정합니다(Brittle). 만약 HttpURLConnection의 새로운 버전이 getResponseCode()의 동작을 변경한다면, 이 테스트는 계속 통과하겠지만 실제 프로덕션 코드는 깨질 수 있습니다. 목 객체는 실제 객체가 아니며, 실제 객체의 계약(contract)을 정확히 모델링한다는 보장이 없습니다.
  • 구현에 결합됩니다. 이 테스트는 processUrlgetResponseCode를 호출한다는 사실에 강하게 결합되어 있습니다. 만약 이 메서드의 구현이 getInputStream을 대신 호출하도록 변경된다면, 테스트는 실패할 것입니다. 하지만 processUrl의 외부 동작은 변경되지 않았으므로 테스트는 여전히 통과해야 합니다.
  • 유지보수가 어렵습니다. HttpURLConnection은 수십 개의 메서드를 가진 복잡한 클래스입니다. 이 모든 메서드의 동작을 정확하게 목 처리하는 것은 어렵고 오류가 발생하기 쉽습니다.

대신, 당신이 소유한 타입으로 외부 의존성을 감싸는 래퍼(wrapper)나 어댑터(adapter)를 작성하세요. 이 래퍼는 당신이 제어하는 간단한 인터페이스를 노출하며, 서드파티 타입의 복잡성을 숨깁니다.

// 당신이 소유한 간단한 인터페이스
public interface HttpClient {
  int getResponseCode() throws IOException;
}

// 서드파티 타입을 감싸는 구현체
public class UrlConnectionHttpClient implements HttpClient {
  private final HttpURLConnection connection;

  public UrlConnectionHttpClient(URL url) throws IOException {
    this.connection = (HttpURLConnection) url.openConnection();
  }

  @Override
  public int getResponseCode() throws IOException {
    return connection.getResponseCode();
  }
}

이제, 당신이 소유한 HttpClient 인터페이스를 목 처리하도록 테스트를 리팩터링할 수 있습니다.

@Test
public void testProcessUrl_connectionFailed() throws Exception {
  HttpClient mockHttpClient = mock(HttpClient.class);
  when(mockHttpClient.getResponseCode()).thenThrow(new IOException());

  Processor processor = new Processor();
  assertFalse(processor.processUrl(mockHttpClient));
}

이 새로운 테스트는 이전 테스트의 모든 문제를 해결합니다.

  • 안정적입니다. HttpURLConnection의 구현이 어떻게 변경되든, HttpClient 인터페이스는 당신이 제어하므로 안정적으로 유지됩니다.
  • 동작에 결합됩니다. 이 테스트는 이제 processUrl의 외부 동작(반환 값)을 검증하며, 내부 구현 세부사항에는 신경 쓰지 않습니다.
  • 유지보수가 쉽습니다. HttpClient는 당신이 정의한 간단한 인터페이스이므로, 목 처리하기가 훨씬 쉽습니다.

핵심은 당신이 소유하지 않은 타입을 직접 목 처리하는 대신, 당신이 소유한 래퍼를 통해 상호작용하는 것입니다. 이렇게 하면 테스트가 외부 라이브러리의 변덕으로부터 격리되어 더 깨끗하고 견고하며 유지보수하기 쉬워집니다. 래퍼에 대한 자체 테스트를 작성하여 서드파티 라이브러리와 올바르게 통합되었는지 확인할 수도 있습니다. 래퍼는 한 번만 작성하면 되지만, 수많은 테스트에서 재사용될 수 있습니다. 래퍼를 사용하세요!