테스트 더블이란?
제라드 메스자로스(Gerard Meszaros)가 사람들이 테스트를 위해 시스템의 일부를 떼어낼 때 사용하는 Stub, Mock, Fake, Dummy 등 다양한 이름들로 불리고 있던 것을 해결하기 위해 만든 용어가 바로 테스트 더블이다.
즉, 테스트 더블이란 테스트 목적으로 프로덕션 객체를 대체하는 모든 경우를 통칭하는 용어이다.
테스트 더블이란 용어는 영화 촬영 시 위험한 역할을 대신하는 스턴트 더블에서 비롯되었다고 한다.
테스트 더블 종류
일반적으로 크게 5가지의 종류로 Dummy, Fake, Stub, Spy, Mock으로 분류된다.
Dummy
- 가장 기본적인 테스트 더블이다.
- 객체가 전달되지만 사용되지 않는 객체다.
- 보통 매개변수 목록을 채우는 데만 사용된다.
- 동작하지 않아도 테스트에는 영향을 미치지 않은 테스트 객체.
- 정말 말 그대로 모조 객체. 객체인 척만 한다.
- 테스트 핵심 로직과는 상관없는 객체이다.
Fake
- 복잡한 로직이나 객체 내부에서 필요로 하는 다른 외부 객체들의 동작을 단순화하여 구현한 객체이다.
- 실제 동작의 구현을 가지고 있지만, 실제 프로덕션에는 적합하지 않는 객체를 의미한다
예시로는 DAO 혹은 repository의 inmemory 구현이 있다.
Stub
- 스텁은 미리 정의된 데이터를 보관하고 테스트 중에 호출에 응답하는 객체이다.
- 테스트를 위해 프로그래밍된 내용에 대해서만 준비된 결과를 제공한다.
- 쉽게 말해 인터페이스 또는 기본 클래스가 최소한으로 구현된 상태다.
Spy
- 스파이는 일부 기록 기능이 있는 테스트 스텁이다.
- Stub의 역할을 하면서 호출된 내용에 대한 정보를 기록한다.
- ex. 이메일 서비스를 목킹할 때 전송된 메시지 수를 기록할 때 사용할 수 있다.
- 실제 객체처럼 동작시킬 수도 있고, 필요한 부분에 대해서는 Stub으로 만들어서 동작을 지정할 수 있다.
- 실제 객체로도 사용할 수 있고, Stub 객체로도 활용할 수 있으며 필요한 경우 특정 메서드가 제대로 호출되었는지 여부를 확인할 수 있다.
- 상태를 가질 수 있어 대표적인 상태 검증 테스트 더블이다.
Mockito 프레임워크의 verify() 메서드가 같은 역할을 한다.
Mock
- 호출에 대한 기대를 명세하고 내용에 따라 동작하도록 프로그래밍된 객체를 의미한다.
- 프로덕션 코드를 호출하고 싶지 않거나 의도한 코드가 실행되었는지 쉽게 확인할 방법이 없을 때 사용한다,
- 따라서 다른 테스트 더블과는 다르게 행위 검증 사용을 추구한다.
Mockito 프레임워크가 대표적인 Mock 프레임워크라 볼 수 있다.
단위테스트
공통적인 속성
마틴 파울러가 정리한 '유닛테스트'에 대한 다양한 의견 중 공통점은 다음과 같다.
- there is a notion that unit tests are low-level, focusing on a small part of the software system.
- 단위 테스트는 소프트웨어 시스템의 작은 부분들에 집중하는 로우 레벨이다.
- unit tests are usually written these days by the programmers themselves using their regular tools
- 일반적인 도구를 사용해서 스스로 단위 테스트를 작성한다
- Thirdly unit tests are expected to be significantly faster than other kinds of tests.
- 단위 테스트는 다른 테스트 종류들의 테스트에 비해 확실하게 빠르기를 기대한다.
단위의 기준
그렇다면 가장 헷갈리는 부분은 바로 단위이다. 무엇이 단위가 되어야 할까? 단위의 범위는 어디까지일까?
마틴 파울러는 단위는 상황적이라고 언급한다. 무엇이 단위가 되는 것인지는 팀이나 개인이 (그때그때) 정하는 것이라고 한다.
또한, 이런 것을 정의하는 것은 전혀 중요하지 않는다고 한다. 클래스를 하나의 단위로 취급할수도 있고 클래스 메서드들의 부분 집합을 하나의 단위로 삼을 수도 있다.
Sociable & Solitary
따라서 단위 테스트를 말할 때 더 중요한 차이는 단위가 단독(Solitary)으로 진행할지 혹은 협동적(Sociable)으로 진행할지 정의하는 데 있다고 한다.
Mock 객체를 이용하여 고립되어 단독으로 실행하는 것을 지향하는 'Mockist`파와 Mock객체를 사용하지 않고 실제 의존하고 있는 객체를 이용하여 협동적인 테스트를 지향하는 'Classist'파로 나뉜다고 한다.
즉 SUT의 협력 객체를 실제 객체로 사용하는지 Mock 객체로 사용하는지에 따라 구분되어지는 것이다.
Classist vs Mockist
Classist
- 협력 객체를 실제 객체를 사용한다.
- 실제 객체를 사용하기 때문에 협력 객체의 상세 구현에 대해서 알 필요가 없다.
- 행위가 끝난 후 직접적으로 상태를 검증을 한다.
- 테스트의 안정성은 높아질 수 있다.
- 비결정적인 외부 요인은 테스트 더블 사용이 가능하다.
- 테스트와 테스트간의 격리
Mockist
- 협력 객체를 Mock 객체를 사용한다.
- 하지만 테스트가 협력 객체의 상세 구현을 알아야 한다.
- 객체 내부의 상태가 아닌 행위를 검증한다.
- 비교적 테스트의 안정성은 낮아질 수 있다.
- 테스트가 상세 구현에 의존하는 경향이 생긴다.
- SUT와 협력 객체간의 격리
Inside-Out vs Outside-In
이렇게 격리의 단위에 따라 테스트의 구현 방법이 달라지므로 자연스럽게 TDD의 방향도 달라지게 된다.
주로 Classist는 Inside out 방법을 사용하고 Mockist는 outside in 방법을 사용한다.
Inside-Out
일반적으로 도메인부터 시작하여 개발을 진행한다. 도메인에 필요한 것은 무엇인지 생각하여 도메인 개체가 필요한 작업을 수행한다.
이후 작업이 완료되면 그 위에 사용자와 맞닿는 영역을 구현한다. 따라서
- 리팩토링 단계에서 디자인이 도출된다.
- TDD에서 빠른 피드백이 가능하다.
- 오버 엔지니어링을 피하기 쉽다.
- 객체 간의 협력이 이상하거나 public api가 잘못 설계될 수 있다.
Outside-In
일반적으로 시스템 외부에 대한 첫 테스트를 작성한다. 요구사항을 만족하기 위해 협력 객체들에 대해 생각한다.
이후 작업이 완료되면 시스템 내부적으로 들어가며 구현한다. 따라서
- Test Red 단계에서 디자인이 도출된다.
- 오버 엔지니어링으로 이어질 수 있다.
- 협력 객체의 public api가 자연스럽게 도출된다.
- 객체들 간의 구현보다는 행위에 집중할 수 있기 때문에 객체지향적으로 바라보기 쉽다.
그래서?
둘 중의 한 가지 방식을 선택하는 개념의 문제가 아니다. 켄트백의 TDD에서는 방향성을 가질 필요가 있다면 아는 것에서 모르는 것으로(known-to-unknown)의 방향이 유용할 수 있다고 말한다.
개인적으로는 보통 요구사항을 단위로 전달받아 주로 개발이 진행되므로 인수테스트를 작성하여 전반적인 요구사항의 전반적인 이해를 먼저 진행한 뒤에 도메인부터 구현하는 방법도 유용할 수 있을 것이다.
참고
'test' 카테고리의 다른 글
[Cucumber] Scenario Outline vs Data Table (1) | 2024.11.27 |
---|