이번 글에서는 단위 테스트에서 설명해주는 목에 대해 알아보겠습니다.
테스트와 목(Mock)
목은 세부 분류로 들어가면 더미, 스텁, 스파이, 목, 페이크의 다섯 가지 유형으로 구분됩니다.
이 다섯 가지 유형은 크게 두 가지 분류로 나뉘어서 목과 스텁의 두 가지 분류로 구분됩니다.
목(Mock)
목은 외부로 나가는 상호 작용을 모방하고 검사하는데 도움이 되는 도구입니다. 이러한 상호 작용은 SUT가 상태를 변경하기 위한 의존성을 호출하는 것에 해당합니다. 대표적인 예시로는 이메일 발송과 같이 Side effect가 있는 호출이 있습니다.
목은 다시 목(mock)과 스파이(spy)의 두 분류로 나눠지는데, 스파이는 목과 동일하지만 단지 프로그래머가 수동으로 작성한다는 차이만 있습니다. 최근의 Mockito나 Mockk 같은 라이브러리는 스파이의 작성도 라이브러리에서 도와주고 직접 작성할 필요도 없게 만들어주고 있어 목과 스파이의 경계가 점점 모호해지는 것 같습니다. 다만 라이브러리에서 스파이를 만들어 줄 때 반환값을 int나 string 같은 값들만 기본값으로 반환하게 해주는 정책들이 있어서 라이브러리마다 확인하고 개발하는게 좋습니다.
스텁(stub)
스텁은 내부로 들어오는 상호 작용을 모방하는데 도움이 됩니다. 이러한 상호 작용은 SUT가 입력 데이터를 얻기 위한 의존성을 호출하는 것에 해당합니다. 대표적으로 데이터베이스의 데이터 검색과 같은 작업이 있습니다.
스텁은 다시 더미(dummy), 스텁(stub), 페이크(fake)로 나뉘는데 얼마나 더 똑똑하냐에 그 차이가 있습니다.
- 더미는 널 값이나 가짜 문자열 같은 단순한 하드코딩 값입니다
- 스텁은 시나리오마다 다른 값을 반환하게끔 구성할 수 있도록 모든 것을 갖춘 완전한 의존성입니다
- 페이크는 스텁이지만 아직 존재하지 않는 의존성을 대체하고자 구현하는 것입니다. 실제 DB 대신 파일 시스템을 사용하는 등의 대체제를 쓸 때를 말합니다.
저의 경험으로는 실제 테스트를 구현할 때 페이크는 사용하지 않았습니다. 호출 횟수가 총 몇 회인지, 데이터가 어떤 데이터가 들어왔는지 등의 검증을 할 때 사용한다고 예시를 드는데, 요즘의 모킹 라이브러리들은 모킹만 해도 해당 기능들을 지원해주는 상황이 많아서 직접 구현할 상황이 없었습니다.
목과 스텁의 차이
위의 정의에 적었듯이 목은 상호 작용을 모방하고 실제로 호출되었는지 검사까지 진행해야 합니다.
스텁은 내부로 들어오는 상호작용을 모방만 하고 실제로 호출되었는지 검사하지 않습니다.
만약 테스트 코드에서 상호 작용을 모방만 한 코드가 보인다면 스텁으로 이해하면 되고, 상호 작용까지 검증하였다면 목으로 이해하면 됩니다.
스텁 사용시 주의점
스텁은 상호 작용을 모방만 하지 실제로 호출되었는지 검사하면 안 됩니다. 스텁은 목과 다르게 외부로의 호출이 최종 결과가 아니므로 상호 작용을 검증하면 안 됩니다.
예시를 들자면 email 전송 기능은 외부로의 상호 작용이므로 호출이 되었는지가 최종 결과로 인식됩니다. 따라서 목으로 취급하고 실제로 호출 되었는지까지 검증해야 합니다.
데이터베이스의 호출은 외부로의 상호 작용이지만 호출이 되었는지를 최종 결과로 인식하지 않습니다. 따라서 스텁으로 취급하고 실제로 호출되었는지를 검증하면 안 됩니다.
스텁으로의 호출을 검사하게 된다면 이는 과잉명세가 됩니다.
개인적으로 더 공부해보고 싶으시다면 martin fowler가 작성한 목은 스텁이 아니다를 보시면 더 좋겠습니다.
저는 개인적으로 데이터 베이스의 select 호출을 최종 결과로 인식하지 않기 때문에 스텁으로 취급합니다.
사람마다 다르겠지만 만약 데이터베이스 작업을 최종 결과라고 인식하셨다면 실제로 호출했는지까지 검증하시면 됩니다.
모킹할 때의 파라미터
모킹 라이브러리들은 일반적으로 모킹할 함수의 파라미터를 대체할 방법으로 anything을 제공해주어 어떤 데이터가 와도 실행되게 할 수 있습니다.
anything은 건네줄 데이터가 복잡할 경우 테스트 코드를 깔끔하게 유지하는데 도움을 줍니다.
이러한 편리함에 취하면 모킹할 때 모든 파라미터를 anything으로 대체하려는 유혹에 빠지게 됩니다. 이럴경우 목이거나 스텁이거나 상관없이 상호 작용을 아무렇게나 모방하게 되어 제대로 모방했는지 검증되지 않습니다.
def forStubbing(str: String): String = ???
예시로 위와 같은 코드가 있을 때, 모든 파라미터를 anything으로 대체해두었을경우 잠시 테스트를 위해 함수를 호출할 때 무조건 "test"로 호출하게 바꿔두고 이를 원복하지 않아도 테스트는 통과하게 됩니다.
코드 리뷰를 진행하게 되면 이러한 문제를 막을 수 있지만 이러한 문제는 테스트에서 확인되는게 가장 좋습니다.
실무에서도 테스트를 짤 때 실제 데이터를 만들어서 파라미터로 사용해 모킹하고, 만약 불가능한 상황이 생길 경우에만 anything으로 대체하는 것을 추천드립니다. 이렇게만 사용하셔도 위에 적은 것과 같은 상황이 방지되니 불가피한 상황이 아니면 실제 객체를 사용해서 모킹하는 습관을 들이는게 좋습니다.