박종훈 기술블로그

테스트의 두 분파 (Classical and Mockist Testing) (마틴 파울러 - Mocks Aren't Stubs 번역)

원문 : 마틴 파울러 - Mocks Aren’t Stubs

목은 스텁이 아닙니다 (Mocks Aren’t Stubs 번역) 에서 이어지는 글입니다.


고전 테스트와 모의객체 테스트 (Classical and Mockist Testing)

두가지 사이에서 가장 큰 화두는 언제 mock(또는 다른 대역)을 사용하는가 입니다.

고전 TDD 스타일은 가능하면 실제 객체를 사용하고, 실제 객체를 사용하기 불편하면 double을 사용하는 것입니다.
전통적인 TDD 사용자는 warehouse 에는 실제 객체를 사용하고 mail service 에는 대역을 사용합니다. (대역의 종류는 중요하지 않습니다.)

모의객체 TDD 스타일은 흥미로운 동작이 있는 객체에 대해 항상 모의 객체를 사용합니다. 모의객체 TDD 사용자는 warehouse 와 mail service 모두 에 대해서 모의 객체를 사용합니다.

  • warehouse 와 mail service 는 이전 글에서 언급된 사례에 나와있다.
  • Mockist를 상황에 따라 모의객체, 모의주의, 모의주의자 라고 번역하였다.
  • TDDer를 TDD 사용자 라고 번역하였다.

다양한 목 프레임워크들이 모의객체 테스트 하는 것을 염두하고 디자인 되었지만, 고전파들도 대역을 만드는게 유용하다는 것을 알게 되었습니다.

모의객체 스타일의 중요한 파생물은 BDD (Behavior Driven Development) 입니다. BDD는 Daniel Terhorst-North에 의해 개발된 기술로, 테스트 주도 개발을 배우는 데 도움을 주기 위해 TDD가 어떻게 디자인 기술로 작동하는지에 중점을 두는 방법입니다. BDD는 테스트를 행동(behaviors)으로 명명하여, 어떤 객체가 무엇을 해야 하는지에 대한 생각에 TDD가 어떻게 도움을 주는지 확인할 수 있게 해주었습니다. BDD는 모의주의자와 같은 접근 방식을 취하지만 거기서 더 나아가 이름 명명 스타일과 기술 내 분석 (analysis within its technique)을 통합하는 시도를 합니다. 이 글에서는 BDD가 모의주의자처럼 테스트를 사용하려는 경향이 있는 TDD의 변형 라는 것 까지만 언급하고 넘어가겠습니다.

BDD 와 관련된 부분은 BDD를 소개합니다. 에서 다루었습니다.

“고전파”를 “디트로이트” 스타일이라 부르기도 하고 “모의주의자”를 “런던” 스타일이라고 부르기도 합니다. 이는 XP(Extreme Programming)가 원래 디트로이트의 C3 프로젝트로 개발되었고 모의객체 스타일은 런던의 초기 XP 채택자들에 의해 개발되었기 때문입니다.

두 가지 중에서 선택하기 (Choosing Between the Differences)

이 글에서는 상태 검증과 동작 검증, 고전파와 모의주의자 의 차이에 대해서 설명했습니다. 두 가지 중에서 선택을 해야한다면 어떤걸 고려해야 할까요?

먼저 상태 검증과 동작 검증에 대해서 알아보겠습니다. 가장 먼저 고려해야 할 것은 맥락입니다. order와 warehouse 같은 쉬운 협력 관계인가요? 아니면 order와 mail service 같은 어려운 협력 관계인가요?

협력 이라는 것의 해석이 어렵다면 이전 글에서 SUT와 협력자 에 대해서 다시 보고 오면 좋을 것 같다.

쉬운 협력 관계라면 선택은 간단합니다. 내가 고전파 TDD 사용자라면 모의, 스텁 또는 어떤 종류의 대역도 사용하지 않습니다. 실제 객체와 상태 검증을 사용합니다.
내가 모의객체 TDD 사용자라면 모의 및 동작 검증을 사용합니다.
고민할 필요하가 없습니다.

만약 어려운 협력은 어떨까요.
내가 모의객체 TDD 사용자라면 결정할 필요 없이 목과 행동검증을 사용합니다.
내가 고전파 TDD 사용자라면 선택의 여지가 있지만 어느 것을 사용할지는 상황에 따라 가장 쉬운 방법을 선택합니다.

보시다시피 상태 검증 대 동작 검증은 대부분 큰 결정이 아닙니다. 진짜 문제는 고전파 TDD 와 모의객체 TDD 사이에 있습니다. 상태 검증과 동작 검증 각각의 특성이 이 논의에 영향을 미칩니다.

더 자세히 알아보기 전에 좀 더 극단적인 상황을 제시해보겠습니다. 때때로 어려운 협력이 아니더라도 상태 검증이 어려운 상황이 생길 수 있습니다. 이에 대한 좋은 예시는 캐시입니다. 캐시의 요점은 캐시가 적중했는지 아니면 누락되었는지 상태를 통해 알 수 없다는 것입니다. 이러한 상황에서는 고전파 TDD 사용자에 대해서도 동작 검증이 현명한 선택이 됩니다. 이렇듯 두 분파 모두 예외가 있습니다.

Driving TDD

모의 객체는 XP 커뮤니티에서 나왔고, XP의 주요 특징 중 하나는 테스트 작성에 의한 반복을 통해 시스템 설계가 발전하는 테스트 중심 개발에 중점을 둔 것입니다.

따라서 모의주의자들이 모의 객체가 테스트 디자인에 미치는 영향에 대해서 이야기 하는 것이 놀랄일이 아닙니다. 특히 모의주의자들은 필요 중심 개발(need-driven development)이라는 스타일을 옹호합니다. 이 스타일을 통해서 시스템 외부에 대한 첫 테스트를 작성하고 일부 인터페이스 객체를 SUT로 만들어 사용자 스토리 개발을 시작합니다. 협력자가 어떻게 동작할지 생각하면서, SUT와 인접 항목 간의 상호작용을 탐색하여 SUT의 아웃바운드 인터페이스를 효과적으로 설계합니다.

테스트를 실행하면, 목객체에 대한 기대값이 다음으로 해야할 사양(specification)과 테스트 지점(starting point)을 제공합니다. 각 기대값을 협력자에 대한 테스트로 전환하고, 한번에 하나의 SUT로 시스템 내부에 들어가는 프로세스를 반복합니다. 이 스타일은 아웃사이드-인(outside-in)이라고도 하는데, 이는 이 스타일을 매우 잘 설명하는 이름입니다. 계층화된 시스템과 잘 작동합니다. 먼저 아래에 있는 모의 레이어를 사용하여 UI를 프로그래밍 하는 것으로 시작합니다. 그런 다음 하위 계층에 대한 테스트를 작성하고 한 번에 한 계층씩 시스템을 단계적으로 진행합니다. 이는 매우 구조화되고 통제된 접근 방식으로, 많은 사람들이 초보자에게 OO 및 TDD를 안내하는데 도움이 된다고 믿는 접근 방식입니다.

고전 TDD는 다른 지침을 제공합니다. 모의 객체를 사용하는 대신 스텁 메소드(stubbed method)를 이용해서 비슷한 단계별 접근 방식을 수행합니다. 이를 위해 협력자로부터 무엇이 필요할 때마다 SUT가 동작하는데 필요한 응답을 정확하게 하드 코딩하기만 하면 됩니다. 그런 다음 녹색으로 표시되면 하드 코딩된 값을 적절한 코드로 변경합니다.

고전 TDD는 다른 방식으로도 가능합니다. 대표적으로 미들 아웃(middle-out) 방식이 있습니다. 이 스타일은 기능을 선택하고 이 기능이 작동하기 위해 도메인에 필요한 것이 무엇인지 결정합니다. 도메인 개체가 필요한 작업을 수행하도록 하고 일단 작업이 완료되면 그 위에 UI를 계층화 합니다. 이렇게 하면 아무것도 위조할 필요가 없어집니다. 많은 사람들이 이를 종아하는 이유는 먼저 도메인 모델에 집중하여 도메인 로직이 UI에 유출되는 것을 방지할 수 있기 때문입니다.

나는 모의주의자와 고전파 모두 같은 이야기를 한다는 점을 강조하고 싶습니다.
애플리케이션을 레이어별로 구축하는 학파도 있습니다. 이 학파는 한 레이어가 완료되기 전에 다른 레이어를 시작하지 않습니다.
고전파와 모의주의자 모두 짧은 주기의 반복을 선호하는 애자일의 특징을 가지고 있으며 있습니다. 결과적으로 레이어별로가 아니라 기능 단위로 작업합니다.

픽스처 설정하기 (Fixture Setup)

고전 TDD를 사용하면 SUT뿐만 아니라 SUT가 테스트에 응답하는 데 필요한 모든 협력자를 만들어야 합니다. 예제에는 몇 개의 개체만 있었지만, 실제 테스트에는 많은 양의 보조 개체가 포함되는 경우가 많습니다. 이러한 개체는 테스트를 실행할 때마다 생성되고 제거됩니다.

그러나 모의객체 테스트에서는 SUT를 생성하고 바로 인접 항목을 모의 객체로 만듭니다. 이렇게 하면 복잡한 픽스처를 구축하는 것을 피할 수 있습니다. (모의 객체를 사용해도 복잡한 경우가 있다고 하지만, 이는 도구를 제대로 사용하지 못했기 때문일 수 있습니다.)

실제로 고전파 테스터들은 복잡한 픽스처를 최대한 재사용하려는 경향이 있습니다. 가장 간단한 방법은 픽스처 설정 코드는 xUnit setup 메서드에 넣는 것입니다. 더 복잡한 픽스처는 여러 테스트 클래스에서 사용해야 하므로 이 경우 특수 픽스처 생성 클래스를 생성합니다. (일반적으로 Object Mother 라고 부릅니다.) 대규모 고전 테스트에서는 Object Mother를 사용하는 것이 필수적이지만 이는 추가적으로 유지보수가 필요한 코드이기 때문에 Mother에 대한 변경사항은 테스트들에게 큰 파급 영향을 미칠 수 있습니다. 픽스처를 설정하는데 성능 이슈를 발생시킬 수 있으나 제대로 수행했을 때 심각한 문제가 되는 경우는 거의 없습니다. 대부분 픽스처의 생성 비용은 저렴하지만, 잘못 수행하면 두 배가 되기도 합니다.

오브젝트 마더 패턴에 대해서는 마틴 파울러의 글 ObjectMother를 참고하자.

서로에 대해서 상대의 스타일이 너무 많은 일을 한다고 하는 것을 들었습니다.
모의주의자들은 고전파에게 픽스처를 만드는 것이 많은 노력이 필요하다고 말하고, 고전파는 모의주의자에게 자신들의 픽스처는 재사용 가능하지만, 모의주의자들은 매 테스트마다 모의 객체를 만들어야 한다고 말합니다.

테스트 격리 (Test Isolation)

모의객체 테스트를 사용하는 시스템에 버그가 발생되면 일반적으로 SUT에 버그가 포함된 테스트만 실패하게 됩니다. 반면에 고전적인 테스트를 사용하는 경우에는 클라이언트 개체에 대한 모든 테스트도 실패할 수 있으며, 이로 인해 보그가 있는 개체가 다른 개체의 테스트에서 협력자로 사용되는 경우에도 오류가 발생합니다. 결과적으로 자주 사용되는 개체에 오류가 발생하면 시스템 전체에 걸쳐 테스트 실패가 전달됩니다.

모의주의자 테스터들은 이것이 주요 문자라고 생각합니다. 오류의 원인을 찾고 수정하기 위해 많은 디버깅이 필요하기 때문입니다. 그러나 고전주의자들은 이것을 문제의 근원이라고 표현하지 않습니다.
일반적으로 어떤 테스트가 실패했는지 살펴보면 문제의 원인을 비교적 쉽게 찾아낼 수 있으며, 개발자는 다른 실패가 근본 결함에서 파생되었다는 것을 알 수 있습니다. 만약 정기적으로 테스트를 진행했다면, 마지막으로 수정한 내용으로 인해 손상이 발생했음을 알 수 있으므로 결함을 찾는 것이 어렵지 않습니다.

여기서 중요한 요소 중 하나는 테스트의 세분성입니다. 고전 테스트는 여러 실제 개체를 실행하므로 개체 클러스터에 대한 기본 테스트로 하나가 아닌 단일 테스트를 사용하는 경우가 많습니다. 해당 클러스터가 많은 개체에 걸쳐 있으면 버그의 실제 소스를 찾는 것이 훨씬 더 어려울 수 있습니다. 여기서 발생하는 현상은 테스트가 너무 거칠게 (coarse grained) 이루어지고 있다는 것입니다.

모의객체 테스트에서는 이 문제가 발생할 가능성이 적습니다. 왜냐하면 기본 객체 외에는 모든 객체를 모의하는 것이 관례이기 때문입니다. 이는 협력자들에게 더 세밀한 테스트가 될 수 있도록 합니다.
여기서 지나치게 대략적인 테스트로 인한 문제는 고전적인 테스트의 문제라기 보다는 고전적인 테스트를 제대로 수행하지 못했기 때문이라고 보는것이 옳습니다. 경험상 좋은 규칙은 모든 클래스에 대해 세분화된 테스트를 분리하는 것입니다. 클러스터는 때때로 합리적이지만 매우 적은 수의 개체(6개 이하)로 제한되어야 합니다. 또한 지나치게 대략적인 테스트로 인해 디버깅 문제가 발생하는 경우 테스트 중심 방식으로 디버그하여 점점 더 세분화 된 테스트를 만들어야 합니다.

여기서 이야기 하는 클러스터가 단위 테스트의 두 분파 (고전파와 런던파) 에서 나온 클래스 세트를 의미하는 것으로 보인다. “단위가 반드시 클래스에 국한될 필요는 없다. 공유 의존성이 없는 한 여러 클래스를 묶어서 단위 테스트할 수도 있다.”

본질적으로 고전적인 xunit 테스트는 단순한 단위 테스트가 아니라 미니 통합 테스트이기도 합니다. 결과적으로 (고전파를 선호하는) 많은 사람들은 객체에 대한 기본 테스트, 특히 클래스가 상호 작용하는 영역을 조사할 때 놓칠 수 있는 오류를 클라이언트 테스트가 포착할 수 있다는 사실을 좋아합니다. 모의객체 테스트는 이러한 부분을 놓칠 수 있습니다. 게다가 목 객체의 기대값이 잘못되는 경우, 단위 테스트는 통과하지만 가려진 오류를 찾지 못할 수 있습니다.

단위 테스트는 통과하지만 가려진 오류를 찾지 못하는 현상 : 거짓 음성

거짓 양성과 거짓 음성 거짓 양성(false positive) : 통계상 실제로는 음성인데 검사 결과는 양성이라고 나오는 것이다.
거짓 음성(false negative) : 통계상 실제로는 양성인데 검사 결과는 음성이라고 나오는 것이다.

예를 들어, 어떤 메일이 스팸 메일인지 검사하는 프로그램이 있다고 하자.
이때 어떤 메일이 실제로는 스팸 메일이 아니지만 프로그램 검사 결과 스팸 메일이라고 판정한다면, 이것이 거짓 양성이다.
이때 어떤 메일이 실제로는 스팸 메일임에도 불구하고 프로그램 검사 결과 스팸 메일이 아니라고 판정한다면, 이것이 거짓 음성이다
출처: 위키피디아

어떤 테스트 스타일을 사용하든 시스템 전체에 걸쳐 작동하는 보다 대략적인 승인 테스트와 결합하여 방지할 수 있습니다.

구현에 대한 결합 테스트 (Coupling Tests to Implementations)

모의객체 테스트를 작성할 때 SUT의 아웃바운드 호출을 테스트하여 공급자(suppliers)와 제대로 통신하는지 확인합니다. 고전 테스트는 오직 최종 상태에만 관심이 있으며, 해당 상태가 어떻게 나온것인지는 고려하지 않습니다. 따라서 모의객체 테스트는 메스드 구현과 더 많이 결합됩니다. 협력자에 대한 호출방식을 변경하면 일반적으로 모의객체 테스트는 중단됩니다.

이러한 결합은 몇 가지 우려를 야기합니다. 가장 중요한 것은 테스트 주도 개발에 미치는 영향입니다. 모의객체 테스트 방식으로 테스트를 작성하면 동작의 구현에 대해서 생각을 하게 됩니다. 이 부분에 대해서 모의객체 테스트는 장점으로 봅니다. 그러나 고전주의자들은 외부 인터페이스에서 무슨 일이 일어나는지만 생각하고 구현에 대한 모든 고려 사항은 테스트 작성이 끝날 때까지 남겨두는 것이 중요하다고 생각합니다.

구현에 대한 결합은 리팩터링에도 방해가 됩니다. 왜냐하면 구현 변경이 발생되면 테스트가 중단될 가능성이 훨씬 높기 때문입니다.

이는 목 툴킷의 특성으로 인해 더욱 악화될 수 있습니다. 종종 모의 도구는 이 테스트와 관련이 없는 경우에도 구체적인 메소드 호출과 파라미터 일치를 지정하도록 합니다.

디자인 스타일 (Design Style)

이러한 테스트 스타일의 흥미로운 측면 중 하나는 디자인 결정에 어떤 영향을 미치는가입니다.

모의객체 테스트는 외부 접근 방식(outside-in)을 선호하는 반면, 도메인 모델 외부 스타일을 선호하는 개발자는 고전 테스트를 선호하는 경향이 있습니다.

더 작은 수준에서 나는 모의객체 테스터들은 값을 반환하는 메서드보다 수집 객체(collecting object)에 대해 동작하는 메서드를 선호하는 경향이 있다는 것을 발견하였습니다.
보고서를 위한 문자열을 생성 동작의 경우를 예시로 들어보겠습니다. 이를 수행하는 일반적인 방법은 보고 메서드에서 다양한 객체에서 문자열 반환 메서드를 호출하고 그 결과를 임시 변수에서 조합하는 것입니다. 모의객체자 테스터는 대신 문자열 버퍼(string buffer)를 다양한 객체에 전달하고 이들에게 다양한 문자열 버퍼를 추가하도록 요청하는 경향이 더 있으며, 문자열 버퍼를 수집 매개변수로 처리합니다.

모의주의자 테스트들은 열차 사고(train wrecks)를 피하는 방법에 대해 많이 이야기 합니다. 열차 사고는 메서드 체인 형식 (.e.g getThis().getThat().getTheOther()) 의 사용을 의미하며, 이러한 메서드 체인을 피하는 것은 Demeter의 법칙을 따르는 것으로 알려져 있습니다.

Demeter의 법칙(Law of Demeter)

  • 각 단위는 다른 단위에 대해 제한된 지식만 가져야 합니다. 즉, 현재 단위와 “밀접하게” 관련된 단위만 알아야 합니다.
  • 각 유닛은 자신의 친구들과만 대화해야 합니다. 낯선 사람과 이야기하지 마십시오.
  • 가까운 친구들에게만 이야기하세요.

출처 : 위키피디아

메서드 체인은 코드에 냄새(smell)를 발생시킬 수 있지만, 그 반대로 전달 메서드로 가득 찬 중간 객체(middle men objects bloated with forwarding methods) 역시 냄새를 발생시킬 수 있습니다. (그래서 Demeter의 법칙 보다는 Demeter의 제안 이 더 어울리는 표현인 것 같습니다.)

객체지향(OO) 디자인에서 사람들이 가장 이해하기 어려워 하는 것 중 하나는 클라이언트 코드에서 수행하기 위해 객체에서 데이터를 추출하는 대신 개체에 작업을 수행하도록 권장하는 “묻지 말고 말하기” 원칙입니다. 모의객체 테스트를 사용하면 이러한 getter가 남발되어 있는 상황을 피하는데 도움이 된다고 말하지만. 고전주의자들은 이를 수행하는 다른 방법이 많이 있다고 주장합니다.

묻지 말고 말하기도 후에 기회가 되면 번역해보겠습니다.

상태 기반 검증의 알려진 이슈는 확인을 위해 쿼리 메소드(query methods)를 생성할 수 있다는 것입니다. 태스트를 위해 객체의 API에 메서드를 추가하는 것은 불편합니다. 동작 확인을 사용한다면 해당 문제를 피할 수 있습니다. 이에 대한 반론은 그러한 수정이 실제로는 일반적으로 사소하다는 것입니다.

모의주의자들은 역할 인터페이스를 선호하며 이러한 스타일의 테스트를 사용하면 각 협업이 별도로 목 객체로 대체되므로 역할 인터페이스로 전환될 가능성이 더 높아진다고 주장합니다.
예를들어 보고서를 생성하는 데 문자열 버퍼를 사용하는 경우, 모의객체 개발자는 해당 도메인에서 의미 있는 특정 역할을 고안할 가능성이 높으며, 이 역할은 문자열 버퍼로 구현될 수 있습니다.

디자인 스타일의 이러한 차이가 대부분의 모의주의 개발자들에게 중요한 동기 부여 요소입니다. TDD의 기원은 진화적 설계를 지원하는 강력한 자동 회귀 테스트를 얻고자 하는 것이였습니다. 그 과정에서 실무자들은 테스트를 작성하는 것이 디자인 프로세스에 상당한 향상을 가져온다는 것을 발견하였습니다. 목 객체 지향 개발자들은 어떤 종류의 디자인이 좋은 디자인인지에 대한 강력한 아이디어를 가지고 있으며, 이 디자인 스타일을 개발하는데 도움이 되도록 목 라이브러리를 만들었습니다.

나는 고전파가 되어야 하는가 모의주의자가 되어야 하는가 (So should I Be a classicist of a mockist?)

이 질문에 대답하기는 어렵다. 개인적으로 나는 항상 고전파 TDD 사용자 였으며 지금까지 변경할 이유가 없었다. 나는 모의객체 TDD에 대한 어떤 강력한 이점도 보지 못했으며 테스트를 구현에 결합한 결과에 대해 우려합니다.

모의주의 프로그래머를 보았을때 충격을 받았습니다. 결과의 상태가 아닌 동작이 어떻게 수행될 것인지에 대해 초점을 두고 끊임없이 생각하는 방식이 내게는 매우 부자연스럽게 느껴졌습니다.

나는 작은 프로젝트 외에는 모의객체 TDD를 시도해보지는 않았습니다. 진지하게 시도하지 않고서는 기술을 판단하기 어려운 경우가 많습니다. (다행히) 저는 많은 훌륭한 모의주의 개발자들을 알고 있습니다.
저는 여전히 고전주의자 이지만, 여러분이 스스로 결정을 내릴 수 있도록 두 가지 주장을 최대한 공정하게 제시하였습니다.

따라서 모의객체 테스트가 매력적으로 보인다면 시도해 볼 것을 제안합니다.
일부 영역에서 문제가 있을 경우 특히 시도해 볼 가치가 있습니다. 두 가지 주요 영역이 있습니다. 하나는 테스트가 완전히 중단되지 않고 문제가 어디에 있는지 알려주지 않기 때문에 테스트가 실패할 때 디버깅 하는데 많은 시간을 소비하는 경우입니다. (고전 TDD 에서는 클러스터를 더 세분화하여 이를 개선할 수 있습니다.) 두 번째 영역은 객체에 충분한 동작이 포함되어 있지 않은 경우 모의객체 테스트를 통해 개발 팀이 더 많은 동작이 풍부한 객체를 생성하도록 장려할 수 있습니다.

마무리

단위 테스트, xunit 프레임워크 및 테스트 주도 개발에 대한 관심이 높아짐에 따라 점점 더 많은 사람들이 모의 객체를 사용하고 있습니다. 많은 경우 사람들은 모의주의적 방식과 고전적 방식의 구분을 완전히 이해하지 못한 채 목 프레임워크에 대해 얇게 배웁니다. 어느 쪽을 선택하든 관점을 차이를 이해하는 것이 유용하다고 생각합니다. 모의주의 방법론을 따를 필요는 없지만, 목 프레임워크를 유용하게 활용하려면 소프트웨어 설계 결정을 이끄는 사고 방식을 이해하는 것이 유용합니다.

이 글의 목적은 이러한 차이점을 지적하고 이들 사이의 장단점을 제시하는 것이였습니다. 모의주의적 사고에는 이 글에서 다룬것보다 더 많은 것이 있으며, 특히 이것이 설계 스타일에 미치는 영향은 더 그렇습니다.
나는 앞으로 이에 대해 더 많은 글을 보게 되기를 바라며, 이를 통해 코드 이전에 테스트를 작성하는 것의 놀라운 결과에 대한 우리의 이해가 깊어지길 바랍니다.