TotT: 당신이 소유하지 않은 타입은 목킹(Mock)하지 마세요
원문: 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)을 정확히 모델링한다는 보장이 없습니다. - 구현에 결합됩니다. 이 테스트는
processUrl
이getResponseCode
를 호출한다는 사실에 강하게 결합되어 있습니다. 만약 이 메서드의 구현이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
는 당신이 정의한 간단한 인터페이스이므로, 목 처리하기가 훨씬 쉽습니다.
핵심은 당신이 소유하지 않은 타입을 직접 목 처리하는 대신, 당신이 소유한 래퍼를 통해 상호작용하는 것입니다. 이렇게 하면 테스트가 외부 라이브러리의 변덕으로부터 격리되어 더 깨끗하고 견고하며 유지보수하기 쉬워집니다. 래퍼에 대한 자체 테스트를 작성하여 서드파티 라이브러리와 올바르게 통합되었는지 확인할 수도 있습니다. 래퍼는 한 번만 작성하면 되지만, 수많은 테스트에서 재사용될 수 있습니다. 래퍼를 사용하세요!