목차
- 서론
- 테스트 더블이란
- 테스트 더블의 종류
- Test Stub
- Fake
- Test Spy
- Mock
- Dummy
- 결론
서론
이 블로그의 여러 포스팅에서 Test Double 이라는 표현이 자주 등장한다.
이를테면 제어 불가능한 것을 제어하자 (1) - static method 를 mocking 하기 힘든 이유 같은 글에서 말이다.
오늘은 이 테스트 더블에 대해서 알아보려 한다.
Test Double, 테스트 더블이란
영화나 드라마에서, 무술 장면이나 실제 배우가 출연하기 힘든 위험한 장면을 촬영할 때 그 분야에 전문적으로 숙달되어 있는 사람을 고용하는데, 이들을 스턴트맨 혹은 스턴드 더블이라고 부른다.
스턴트 더블은 특정 영상 촬영분을 위해 최적화된 전문가가 대신해서 연기를 수행하는 것이라고도 생각할 수 있다.
테스트 더블이라는 용어도 스턴트 더블에서 나오게 된 테스트 전용 언어이다.
Test Double 은 xUnit Test Patterns의 저자인 제라드 메스자로스(Gerard Meszaros)가 만든 용어이다.
테스트 더블도 역시 특정 영상 촬영분, 즉 특성 상황을 가정하는 테스트에서 테스트 대상, SUT 와 크게 관련이 없는 협력 객체들을 테스트 전용 객체로 대체해서 SUT 를 더욱 잘 테스트하기 위한 방법이다.
우리는 단위테스트를 한다면 숱하게 듣는 이야기가 있다.
단위 테스트는 SUT 를 통제할 수 있는상황으로 격리시키는 것이 중요하다.
SUT 를 통제할 수 없는 상황이란 무엇일까?
아래의 상황을 보자
public class LogAlerter {
private final FileLoader loader;
private final Notifier notifier;
public void alert() {
File file = loader.load("2022-12-12.log");
String line = file.readFirstLine();
String header = line.substring(0, 10);
if (header.equals("HEALTHY")) {
return;
}
notifier.doNotify(line);
}
}
간략하게 위의 코드를 설명하면 다음과 같다.
- log file 을 로컬에서 불러와서 한줄을 읽는다.
- 해당 로그 파일의 첫번째 라인에 HEALTHY 라는 키워드가 존재하지 않으면 알림을 전송한다
이 코드를 테스트한다고 하면 어떻게 테스트할 수 있을까?
LogAlerter 라는 클래스와 협력하는 두개의 클래스가 있는 것을 알 수 있다.
- FileLoader
- Notifier
이 코드를 테스트한다면 아마 다음과 같은 문제가 생길 것이다.
- 테스트를 돌리는 머신에 항상 2022-12-12.log 라는 파일이 존재해야 한다.
- 다른 개발자가 내가 짠 테스트 코드를 내려받고 실행할 때는 항상 2022-12-12.log 가 존재해야 하나?
- CI 환경에서는 어떻게 하지? Build Runner 머신에서도 2022-12-12.log 가 존재해야 하나?
- HEALTHY 상태가 아니라면 알림을 전송한다
- 그럼 테스트에 실패하면 항상 알림을 전송할까?
- 알림 받는 휴대폰? 슬랙에는 그럼 계속 알림이 올것이고 결국 양치기 소년 알림이 되겠네?
위의 문제는 테스트 대상에 대한 격리가 적절히 이루어지지 않아서 발생하는 문제들이다.
이때가 바로 테스트 더블이 필요한 시점이다.
테스트 대상의 코드와 협력하는 객체들을 테스트 더블로 대체하여 격리를 시킨다면 다음과 같은 형태가 될것이다.
이렇게 됨으로써 테스트 대상 코드를 주변 환경으로부터 독립적이게 동작하게 만들 수 있고 특정 상황을 마음대로 조작함으로 테스트 환경을 장악한 것이다.
결국 테스트 더블은 테스트하려는 대상 코드를 주변에서 분리하도록 도와주는 것 이라고 정의할 수 있다.
이렇게 테스트 더블을 사용함으로써 얻어지는 장점은 여러가지가 있는데 그중에 대표적으로는 다음과 같은 것들이 있다.
- 테스트 대상 코드를 격리한다.
- 테스트 속도를 개선한다.
- 예측 불가능한 실행 요소를 제거한다
- 특정 상황을 시뮬레이션 한다
- 감춰진 정보를 얻어낸다
테스트 더블의 종류
테스트 더블은 다양한 종류가 존재한다.
각각의 테스트 더블은 종류가 존재하고 쓰임새가 있다.
아래는 테스트 더블에 대해서 가장 정리된 그림이다.
크게 5가지의 테스트 더블의 종류가 존재한다.
- Stub
- Fake
- Spy
- Mock
- Dummy
테스트 더블의 종류 1. Stub
테스트 스텁은 원래의 구현을 최대한 단순한 것으로 대체하는 것이다.
사전적으로 테스트 스텁은 끝이 잘렸거나 유난히 짧은 것이라고 한다.
테스트 스텁을 사용하는 목적은 원래의 구현을 최대한 단순한 것으로 대체해서 특정 테스트 케이스에 대해서 만족하기만 하면 될 때 사용한다.
위의 예시에서 로그에 HEALTHY 상태가 아니라면 notify 를 보낸다고 했는데, 이를 협력하는 객체를 스텁으로 대체하면 다음과 같다.
public class NotifierStub {
public void doNotify() { }
}
이처럼 최대한 단순하게 notifier 의 구현을 비워버렸다.
이것이 전형적인 스텁의 모습이다.
만약 어떤 값을 반환하는 메서드라면 스텁에서는 단순히 해당 값만 하드코딩하여 반환하기도 한다.
public class NotifierStub {
public int getNotifiedCount() {
return 20;
}
}
테스트 더블의 종류 2. Fake
가짜 객체, Fake 는 진짜 객체의 행동을 흉내내는 것이다.
테스트 스텁을 보면, 특정 값을 하드코딩할 때가 있다는 것을 다르게 이야기하면 테스트 케이스마다 스텁을 계속해서 바꿔줘야 한다는 것을 의미한다.
이에 반해서 가짜 객체는 테스트 스텁보다 조금 더 노력을 들인 테스트 더블이다.
다음과 같은 상황을 가정해보자 User 엔티티를 영속화하는 상황에서 Repository 패턴을 사용했다고 해보자.
해당 레포지토리를 테스트 스텁으로 만든다면 모든 테스트 케이스에 대해서 스텁을 만들어줘야 한다.
하지만 이렇게 할 수도 있다.
public class FakeUserRepository {
private final List<User> users = new ArrayList<>();
public void save(User user) {
return 20;
}
public User findById(Long userId) {
for (User user : users) {
if (user.id == userId) return user;
}
return null;
}
}
레포지토리가 실제 영속성 장치와 연결되는것이 아니라 로컬 메모리에서 저장되는 형태가 된다면 여러 상황의 테스트 케이스를 커버할 수 있게 된다.
테스트 더블의 종류 3. Spy
테스트 스파이는 내부의 기밀을 훔쳐본다
테스트 spy 는 내부의 기밀을 훔쳐볼 수 있는 스텁이다.
주로 화이트박스 테스트에서 사용될 수 있는 테스트 더블이다.
위의 예시에서 만약 doNotify()
가 호출이 되었는지, 몇번 호출이 되었는지를 알고싶을 때가 있다.
이럴 때 바로 테스트 스파이를 사용한다.
public class SpyNotifier {
int called = 0;
public void doNotify() {
called++;
}
}
이런식으로 호출에 대해서 called 카운트를 증가시키고 테스트 단언문에서 호출 횟수를 검사할 수 있다.
테스트 더블의 종류 4. Mock
mock 은 Stub 와 Spy 를 합쳐놓은 것이다.
Mock 은 호출했을 때 사전에 정의된 명세대로의 결과를 돌려주도록 미리 프로그램 돼있는 것이다.
예상치 못한 호출이 있을 경우 예외를 던질 수 있으며 모든 호출이 예상된 것이었는지 확인할 수 있다.
Mock 객체는 정의된 명세대로의 결과를 돌려준다는 점에서 Stub이 될수도 있고 객체 정의 목적을 호출 되었을 때 방법/과정 등 확인이 필요한 부분을 기록하느냐에 따라서 Spy도 될수도 있다.
java 에서는 mock 과 관련하여 다양한 라이브러리를 제공하는데, 그중 mockito 라는 라이브러리가 유명하며 이 블로그에서는 JUnit5 BDDMockito로 알아보는 TDD와 BDD 의 차이 및 BDD 실습 라는 글에서도 많이 사용하기도 했다.
하지만 martin fowler 는 mocks are not stubs 라고 표현하기도 한다
mock 은 fluent 하게 사용될 수 있는 테스트 스텁이라는 특성 덕분에 mock 객체의 invocation 을 너무나도 자세히 명시하는 테스트 코드도 존재한다.
이렇다는 소리는 테스트가 깨질 수 있는 확률이 증가한다는 것을 의미한다
위의 코드는 필자가 처음 막 mock 라이브러리에 입문했을 때 저질렀던 실수인데, 너무나도 상세한 mocking 으로 인해서 해당 테스트는 fragile 한 테스트 코드가 되어버렸다.
결론
우리는 오늘 4가지 테스트 더블에 대해서 알아보았고 형태를 간단하게 묘사해보았다.
각각 어떤 상황에서 사용해야 할지는 본인의 선택이나 모든 것은 테스트를 읽기 쉽게 만들어주는 선택을 하는 것이 최고의 선택이다.
Effective Unit Testing 에서는 테스트 더블을 선택하는 여러가지 방법들 중에서 마지막으로 이런 말을 한다
이도 저도 아니라면 동전을 던져보자. 앞면이 mock 객체, 뒷면이 stub 이다. 만약 어쩌다 동전이 똑바로 서ㅓ리면 가짜 객체를 써도 뭐라하지 않겠다.
이러한 테스트 더블을 사용하기 위해서는 당연히 Dependency Injection 이 되어야 한다는 사실도 잊지 말자.
public class LogAlerter {
private final FileLoader loader = new FileLoader();
private final Notifier notifier = new Notifier();
public void alert() {
// ...
}
}
위 코드는 테스트 더블을 사용할 수 없다.
만약 테스트 더블을 사용하고 싶다면 런타임에 byte code 를 조작해서 넣는 방법이 있지만 그보다 더 쉽게 테스트 더블을 넣어 환경을 제어할 수 있는 방법이 있다.
바로 Dependency Injection 을 하는 방법이다.
public class LogAlerter {
private final FileLoader loader = new FileLoader();
private final Notifier notifier = new Notifier();
public LogAlerter(FileLoader loader, Notifier notifier) {
this.loader = loader;
this.notifier = notifier;
}
public void alert() {
// ...
}
}
이렇게 종속 객체를 주입할 수 있다면 LogAlerter 를 생성하는 시점에 Stub 객체던 Fake 던 Mock 이던 주입하여 환경을 통제할 수 있게 되는 것이다.
References
'더 좋은 개발자 되기 > 나의 개발론' 카테고리의 다른 글
백엔드 개발자로 밥벌이 1년을 축하하며 2022년의 회고를 (16) | 2022.12.29 |
---|---|
나는 개인적으로 CQRS 를 이렇게 정의내리고 이렇게 생각한다. (9) | 2022.12.06 |
오버엔지니어링 하지 않기 (2) | 2022.08.29 |
제어 불가능한 것을 제어하자 (2) - static method 를 mocking 하는 다양한 방법들 (2) | 2022.07.25 |
제어 불가능한 것을 제어하자 (1) - static method 를 mocking 하기 힘든 이유 (0) | 2022.07.25 |
댓글