테스트 대역(Test Double)로 사용하는 목(Mock)과 스텁(Stub)은 뭐가 다른가
테스트 대상이 되는 SUT(System Under Test)와 협력자(Collaborator) 사이에 일어나는 상호 작용을 검사할 때, 테스트 대역(Test double)을 사용한다. 테스트 대역은 크게 목(Mock)과 스텁(Stub)으로 나눌 수 있다. ’단위 테스트 (블라디미르 코리코프, 2021)’ 책을 보고 정리했다.
테스트 대역(Test double), 목(Mock), 스텁(Stub)
제일 상위 개념인 테스트 대역(Test double)을 먼저 알아보자.
테스트 대역은 모든 유형의 비운영용 가짜 의존성을 설명하는 포괄적인 용어다. 이 용어는 영화 산업의 ’스턴트 대역’이라는 개념에서 비롯됐다. 테스트 대역의 주 용도는 테스트를 편리하게 하는 것이다.
여기서 대역은 프로덕션 코드를 사용하기 곤란할 때 테스트 환경에서만 역할을 대신 맡아 실행하는 코드를 말한다. 예를 들면 핸드폰 문자로 알림을 보내는 코드가 들어있다고 단위 테스트를 할 때마다 실제로 문자를 보낼 수는 없다. 테스트 환경에서만 실제 핸드폰 문자로 알림을 보내는 테스트 대역을 만들고 해당 함수를 호출하는지 검증한다.
테스트 대역이 가장 상위 개념이다. 테스트 대역은 크게 목(Mock)과 스텁(Stub)으로 나눌 수 있다.
어떤 기준으로 목과 스텁을 구분하는 걸까?
- 목은 외부로 나가는 상호 작용을 모방하고 검사하는 데 도움이 된다. 이러한 상호 작용은 SUT(System Under Test)가 상태를 변경하기 위한 의존성을 호출하는 것에 해당한다.
- 스텁은 내부로 들어오는 상호 작용을 모방하는 데 도움이 된다. 이러한 상호 작용은 SUT가 입력 데이터를 얻기 위한 의존성을 호출하는 것에 해당한다.
목과 스텁 중에 제대로 호출하는 지 검사는 목(Mock)에만 들어간다. 스텁은 외부에서 들어오는 입력과 같은 것에 사용한다. 그래서 뭔가 검사할 필요가 없다. 그냥 입력을 모방하는 용도로 내가 입력을 넣기 때문이다. 주식 자동 매매 프로그램을 예로 들어보자. 차트를 보고 매수 매도하는 프로그램이다. 여기서 특정 차트를 흉내 내는 게 스텁이다. 입력으로 쓰인다. 증권사 매수 매도 API에는 목을 사용한다. 매수 매도 API를 제대로 호출하는지 검사하는 로직이 들어간다.
CQS(Command Query Seperation)로 생각할 수도 있다. Command 역할을 하는 의존성은 목으로 대체할 수 있고 Query 역할을 하는 의존성은 스텁으로 대체할 수 있다.
’xUnit 테스트 패턴 (제라드 메스자로스, 2010)’ 책에서는 테스트 대역을 다섯 가지로 세분화했다고 한다. ’단위 테스트 (블라디미르 코리코프, 2021)’ 책에서는 목(Mock)과 스텁(Stub)으로만 세분화해도 충분하다고 말한다. 다섯 가지는 확실히 과하다. 그렇게 테스트 대역을 구분해서 사용하지도 못할 것 같다.
하지만 궁금하다. 다섯 가지로 어떻게 구분했을까?
어떻게 더미(Dummy), 스텁, 스파이(Spy), 목, 페이크(Fake) 다섯 가지로 세분화했을까?
우선 목(Mock)부터 확인해 보자.
모의 객체(Mock Object)도 SUT(System Under Test)의 간접 출력에 대한 관찰 위치로 쓰이는 객체다. 모의 객체는 테스트 스텁과 마찬가지로 메서드가 호출되면 정보를 리턴해야 한다. 또한 테스트 스파이와 마찬가지로 모의 객체에서는 SUT가 자신을 어떻게 호출했는지 신경 쓴다. 테스트 스파이와 다른 점이라면 모의 객체에서는 미리 정의한 기대 값과 실제 호출을 단언문으로 비교해 문제가 있으면 테스트 메서드를 대신해 테스트를 실패시킨다.
xUnit 테스트 패턴 (제라드 메스자로스, 2010) 11장 테스트 대역 사용
’단위 테스트 (블라디미르 코리코프, 2021)’ 책에서는 목과 스파이(Spy)를 묶어서 목으로 단순화했다. 스파이는 어떻게 다른 걸까?
테스트 스파이(Test Spy)는 SUT의 간접 출력에 대한 관찰 위치로 쓰이는 객체다. 테스트 스텁의 기능에 더해 테스트 스파이에서는 SUT가 자신의 메서드를 호출한 내역을 기록할 수 있다. 테스트 검증 단계에서는 테스트 스파이로부터 실제로 호출된 내역과 기대 호출 내역을 여러 단언문(assertion)을 통해 비교하는 절차형 동작 검증을 한다.
xUnit 테스트 패턴 (제라드 메스자로스, 2010) 11장 테스트 대역 사용
목은 단언문을 사용해 실패시키는 것이고 스파이는 호출 내역을 저장 후 검증 단계에서 내역을 불러온다. 스파이란 이름이 참 잘 어울린다. 하지만 이렇게 세분화할 필요는 없다. 동의한다. 그냥 합쳐서 목으로 부르는 게 합리적이다.
이제 스텁으로 합친 스텁, 더미, 페이크를 정의를 보자.
테스트 스텁은 자신의 메서드가 호출될 때, SUT(System Under Test)로 간접 입력 값을 보내는 제어 위치로 사용되는 객체다. 테스트 스텁 덕분에 이전에는 테스트 도중에 실행할 수 없었던 SUT의 테스트 안 된 코드 경로를 실행시킬 수 있다.
xUnit 테스트 패턴 (제라드 메스자로스, 2010) 11장 테스트 대역 사용
스텁은 정의가 비슷하다.
더미 객체(Dummy Object)는 테스트 대역(Test double)이 퇴화된 형태다. 메서드(Method) 에서 메서드로 주고받는 것 외에는 전혀 쓸모가 없다. null을 써도 되지만 null을 받지 않는 코드에서는 실제 객체를 생성해줘야만 한다.
xUnit 테스트 패턴 (제라드 메스자로스, 2010) 11장 테스트 대역 사용
null을 받지 않는 코드에 인자로 넘기려고 만드는 게 더미(Dummy)다.
가짜 객체(Fake Object)는 테스트에 의해 제어되지 않고 관찰되지도 않는다는 점에서 테스트 스텁이나 모의 객체와는 다르다. 가짜 객체는 간접 입력/출력을 검증하려는 것 외에 다른 이유로 테스트에서 실제 DOC(Depended-On Component, 의존 컴포넌트)의 기능을 대체해야 할 사용된다. 보통 가짜 객체에서는 실제 DOC의 기능 중 전체나 일부를 훨씬 단순하게 구현한다. 가짜 객체를 쓰는 가장 일반적인 이유는 실제 DOC가 아직 구현되지 않았거나, 너무 느리거나, 테스트 환경에서는 쓸 수 없기 때문이다.
xUnit 테스트 패턴 (제라드 메스자로스, 2010) 11장 테스트 대역 사용
페이크(Fake)는 스텁과 비슷하다. 다만 스텁은 테스트 안 된 코드 경로를 실행하려고 의도적으로 값을 조작한다. 반면 페이크는 실제 프로덕션 코드를 테스트 용도로 전체나 일부를 구현하는 걸 말한다.
스텁, 더미, 페이크를 합쳐서 스텁으로 단순화 시킨 것도 납득이 된다.
마치며
테스트 대역(Test double)을 두 가지로 세분화할 수 있다. 외부로 나가는 상호 작용을 모방하고 검사하는 데 사용하는 목(Mock)과 내부로 들어오는 상호 작용을 모방하는 데 사용하는 스텁(Stub)이다. CQS(Command Query Seperation)로 생각하면 목은 Command 역할을 하는 의존성을 대체하고 스텁은 Query 역할을 하는 의존성을 대체할 수 있다.
’xUnit 테스트 패턴 (제라드 메스자로스, 2010)’ 책에서는 더미(Dummy), 스텁, 스파이(Spy), 목, 페이크(Fake)로 테스트 대역을 다섯 가지로 세분화했는데, ’단위 테스트 (블라디미르 코리코프, 2021)’ 책에서 단순화시킨 것처럼 스텁과 목으로만 세분화시켜도 충분하다.