모의 객체 이야기 — Endo-Testing에서 Mockito까지

2000년, 매키넌·프리먼·크레이그의 논문이 「모의 객체(mock object)」라는 개념을 제안하며 훗날 런던 학파라 불리는 테스트 사상을 낳았다. GOOS, jMock, Mockito, Sinon.js로 이어지는 계보와 메자로스의 테스트 더블 5분류, 그리고 과도한 모킹의 폐해까지 살펴본다.

testinghistorymockstest-doublestddmockito

TDD(제4기사)의 실천자들은 아주 이른 시기에 한 벽에 부딪혔다. 「테스트 대상 객체가 데이터베이스나 네트워크, 다른 객체에 의존하고 있다면, 단위 테스트는 어떻게 써야 하는가」라는 물음이다. 실제 데이터베이스에 접속하면 테스트가 느리고 불안정해지고, 실제 외부 API를 호출하면 비용이나 부작용이 발생할 수 있다. 그렇다고 의존 대상을 단순히 「아무것도 반환하지 않는 가짜」로 바꾸는 것만으로는, 테스트 대상 객체가 의존 대상을 올바르게 사용하고 있는지까지는 검증할 수 없다. 이 과제에 2000년, 한 논문이 명확한 답을 제시했다.

Endo-Testing 논문 — 2000년, XP2000

2000년, 이탈리아 사르데냐 섬에서 열린 XP2000 컨퍼런스에서 팀 매키넌(Tim Mackinnon), 스티브 프리먼(Steve Freeman), 필립 크레이그(Philip Craig) 세 사람이 “Endo-Testing: Unit Testing with Mock Objects” 라는 논문을 발표했다. 모의 객체라는 개념을 세상에 던진 최초의 체계적인 발표로 여겨진다.

논문 제목의 「Endo(내부)」는 모의 객체가 도메인 코드의 내부에 주입되어, 그 안에서 행동이 관찰된다는 데서 유래한다. 주장은 단순하지만 강력했다.

  • 테스트 대상 객체가 의존하는 협력 객체(콜라보레이터)를, 실제 대신 가짜로 바꿔치기한다
  • 다만 단순히 고정값을 반환하는 데 그치지 않고, 「기대된 호출이 이루어졌는지」를 검증한다 —— 이것이 기존의 「스텁」과의 결정적인 차이였다
  • 모의 객체를 사용하면, 외부 의존을 가진 코드에서도 빠르고 결정론적인 단위 테스트를 작성할 수 있다
  • 「테스트하기 쉬운 설계」를 추구하는 과정에서, 의존 관계가 명시적으로 객체에 주입되는 설계 스타일(의존성 주입)이 자연스럽게 도출된다

이 논문은 단순한 테스트 기법의 제안에 그치지 않고, 「테스트를 작성하는 것 자체가 좋은 설계를 이끈다」는 TDD의 사상을 뒷받침하는 것으로 받아들여졌다.

런던 학파의 탄생

논문의 저자들은 그 후에도 이 실천을 발전시켜, 모의 객체를 적극적으로 사용하며 객체 간의 「협력」과 「대화」를 축으로 설계를 진행하는 스타일을 확립해 간다. 이것이 훗날 런던 학파(London school), 또는 모키스트(mockist) 라 불리는 테스트 스타일의 기원이다.

그 특징은 다음과 같다.

  • 테스트 대상(SUT)이 보내는 메시지(메서드 호출) 그 자체를 검증한다 —— 행동 검증(behaviour verification)
  • 「바깥에서 안으로(outside-in)」 설계를 진행한다 —— 먼저 시스템의 외부 인터페이스를 테스트하고, 그것을 통과시키기 위해 필요한 협력 객체를 모의 객체로 먼저 정의한 뒤, 그다음 구현을 채워 넣는다
  • 객체 간의 책임 분담이나 인터페이스 설계 자체를 TDD의 피드백 루프에 편입시킨다

GOOS — 키우면서 테스트하기(2009)

런던 학파의 사상을 한 권으로 체계화한 것이 스티브 프리먼과 나트 프라이스(Nat Pryce)의 저서 『Growing Object-Oriented Software, Guided by Tests』(통칭 GOOS, 2009년)이다.

이 책은 단일 클래스의 단위 테스트에 그치지 않고, 시스템 전체를 「바깥(사용자에게 보이는 행동)」에서부터 테스트로 이끌며 키워 나가는 방법을, 경매 참가 클라이언트라는 GUI 애플리케이션 개발 사례를 통해 보여주었다. 널리 알려지게 된 개념으로는 다음이 있다.

  • 워킹 스켈레톤(Walking Skeleton): 엔드투엔드로 동작하는 최소한의 골격을 먼저 만들고, 거기에 살을 붙여 간다
  • “Tell, Don’t Ask” 의 철저함과 객체 간 협력 설계
  • 모의 객체는 「아직 존재하지 않는 협력 객체의 명세를 먼저 쓰기」 위한 도구라는 자리매김

’테스트 더블’이라는 어휘의 확립

「모크」「스텁」「페이크」와 같은 말들은 처음에는 개발자마다 제각각으로 사용되었다. 제럴드 메자로스(Gerard Meszaros)는 2004년, 패턴 언어 컨퍼런스 PLoP 2004에서 발표한 논문에서 이들을 포괄하는 명칭으로 「테스트 더블(Test Double)」 이라는 용어를 처음 제안했다. 「더블」은 영화 촬영의 「스턴트 더블(대역)」에서 온 비유로, 「실제를 대신하는 배우」라는 이미지를 정확히 포착하고 있다.

이 용어는 2007년 저서 『xUnit Test Patterns: Refactoring Test Code』 에서 정식으로 체계화되었고, 5가지 분류로 업계 표준 어휘가 되었다.

종류 역할 검증의 성질
더미(Dummy) 인자를 채우기 위해서만 전달되며 실제로는 사용되지 않는다 없음
스텁(Stub) 미리 정해진 값을 반환한다 없음(상태 검증의 보조)
스파이(Spy) 호출을 기록해 두었다가, 나중에 「호출되었는지」를 확인할 수 있다 상태 검증에 가까움
모크(Mock) 호출에 대한 기대를 미리 설정해 두고, 실행 중 또는 실행 후에 검증한다 행동 검증
페이크(Fake) 실제에 가까운 간이 구현(인메모리 DB 등) 상태 검증

이 분류가 중요한 이유는, 「모크」라는 말이 본래는 테스트 더블의 한 종류(행동 검증을 하는 것)에 불과함에도 불구하고, 일상 대화에서는 「테스트 더블 전반」을 가리키는 확대 해석된 말로 쓰이기 쉬운 점을 정리했기 때문이다.

jMock, EasyMock, Mockito의 계보

모의 객체의 실천은 Java의 모킹 라이브러리 군으로 구체화되어 갔다.

라이브러리 특징
jMock Endo-Testing 논문의 저자들이 개발에 관여했다. 엄격한 기대값의 사전 선언을 중시하는, 런던 학파 색이 강한 API
EasyMock 「Record & Replay」라는 2단계 API 스타일로 모의 객체를 생성한다. jMock보다 널리 보급되었다
Mockito 2008년, 런던의 신문사 The Guardian의 시스템 개발 중에 슈체판 파베르(Szczepan Faber)를 중심으로 개발되었다. EasyMock의 코드를 토대로 시작했다

Mockito는 when(...).thenReturn(...)와 같은 읽기 쉬운 API와, 「기본적으로는 느슨한 검증(호출 횟수를 엄밀히 지정하지 않아도 동작한다)」을 특징으로 하며, jMock/EasyMock의 「사전에 모든 기대값을 선언해야 하는」 답답함을 완화했다. 이 사용 편의성 덕분에 Mockito는 2010년대 이후 Java 세계에서 사실상 표준적인 모킹 라이브러리의 지위를 확립했다.

// Mockito: 스텁 설정과 행동 검증을 모두 쓸 수 있다
PaymentGateway gateway = mock(PaymentGateway.class);
when(gateway.charge(1000)).thenReturn(true);

OrderService service = new OrderService(gateway);
service.checkout(order);

verify(gateway).charge(1000); // 호출되었음을 사후에 검증

대조적으로 jMock 계열 API는 「호출이 일어나기 전에 기대값을 선언하는」 스타일을 취한다.

// jMock: 기대값을 먼저 선언한 뒤 실행한다
context.checking(new Expectations() {{
    oneOf(gateway).charge(1000); will(returnValue(true));
}});
service.checkout(order); // 여기서 기대에 어긋나는 호출이 있으면 즉시 실패

이 차이는 사소해 보이지만, 「테스트를 읽었을 때 무엇을 검증하는지 알기 쉬운가」라는 설계 철학의 차이를 반영한다.

JavaScript 세계의 테스트 더블

JavaScript 세계에서는 Sinon.js가 같은 역할을 맡아, 스파이·스텁·모크·페이크 타이머를 통일된 API로 제공하는 라이브러리로 널리 사용되어 왔다.

// Sinon.js: 스텁과 스파이를 조합한 예
const stub = sinon.stub(gateway, "charge").returns(true);
service.checkout(order);
assert(stub.calledWith(1000)); // 스파이처럼 호출 내용을 확인

최근의 JavaScript 생태계에서는 Jest와 Vitest가 자체 모킹 메커니즘(jest.fn()/vi.fn())을 표준으로 탑재하고 있어, 별도의 모킹 라이브러리를 도입하지 않아도 테스트 더블을 다룰 수 있게 되었다. 이 역시 이 분야가 충분히 성숙하여 언어·프레임워크 표준 기능으로 편입되는 단계에 이르렀음을 보여준다.

mockist vs classicist 다시 보기

TDD의 실천에는 크게 두 유파가 있다.

  • 런던 학파(mockist): 협력 객체는 원칙적으로 모두 모의 객체로 대체하고, SUT가 보내는 메시지(행동)를 검증한다. 매키넌·프리먼·프라이스 등의 계보.
  • 디트로이트 학파(classicist): 켄트 벡(Kent Beck)에 가까운 고전적 입장으로, 가능한 한 실제 객체를 사용하고 최종적인 상태(반환값이나 객체의 상태)를 검증한다(상태 검증). 모의 객체는 「실제를 사용하기가 현저히 어려운 경우」(외부 API, 시각, 무작위성 등)에 한정해서 사용한다.

이 대립은 「테스트가 구현의 세부사항을 얼마나 알아야 하는가」라는 설계 철학의 차이에 뿌리를 두고 있으며, 단순한 취향의 문제가 아니라 리팩터링 내성·테스트 의도의 명확함·실행 속도의 트레이드오프와 직결된다.

과도한 모킹이라는 대가

모의 객체의 보급과 함께 널리 인식되게 된 것이 오버 모킹(over-mocking) 문제다.

  • 협력 객체를 모두 모의 객체로 대체하면, 테스트는 「구현이 어떤 메서드를 어떤 순서로 호출하는가」라는 구현의 세부사항을 검증하는 것이 되기 쉬워, 리팩터링할 때마다 테스트가 깨진다
  • 모의 객체 간 상호작용의 설정이 복잡해져, 테스트 코드 자체의 가독성·유지보수성이 떨어진다
  • 「모의 객체가 실제와 같은 행동을 한다」는 전제가 무너지면, 테스트는 통과하는데 실제 운영에서는 깨지는 위음성(false negative)이 발생한다
  • 단위 테스트 수준에서 모의 객체를 과도하게 사용하면, 「테스트는 모두 통과하는데 결합하면 동작하지 않는다」는 전형적인 실패 패턴에 빠진다

이러한 반성으로부터, 오늘날 실무에서는 「모의 객체는 자신이 소유하지 않은 경계(외부 서비스·시각·파일 시스템 등)에만 사용하고, 자기 팀이 소유한 협력 객체는 실제 또는 페이크를 사용한다」는 절충적인 지침이 널리 공유되고 있다. 이 설계 판단은 테스트하기 쉬운 설계와 인터페이스 분할의 문제(제12기사)와도 밀접하게 관련되어 있다.

이 기사에서 다음 기사로

모의 객체는 「특정 호출이 올바르게 이루어졌는가」라는 구체적인 행동을 검증하는 기법이었다. 다음으로 살펴볼 것은 그 정반대의 발상이다. 개별 입출력 예시를 쓰는 대신, 「어떤 입력에 대해서도 성립해야 하는 성질」을 선언하고, 대량의 무작위 입력으로 도구가 이를 검증하게 하는 프로퍼티 기반 테스트——2000년 Haskell 세계에서 태어난 QuickCheck에서 이야기를 시작한다.

참고문헌

  • Tim Mackinnon, Steve Freeman, Philip Craig, “Endo-Testing: Unit Testing with Mock Objects”(XP2000)
  • Steve Freeman, Nat Pryce, Growing Object-Oriented Software, Guided by Tests(2009년)
  • Gerard Meszaros, xUnit Test Patterns: Refactoring Test Code(2007년)
  • Mockito 공식 문서(site.mockito.org)
  • Sinon.js 공식 문서(sinonjs.org)
← Back to 소프트웨어 테스트의 계보