테스트 더블(Test Double)의 유래?
영화 촬영에서 배우를 대신하는 스턴트맨을 들어 본 적이 있을 것이다. 스턴트맨의 또 다른 이름이 스턴트 더블이다.
영화나 드라마에서, 무술 장면이나 실제 배우가 출연하기 힘든 위험한 장면을 촬영할 때 그 분야에 전문적으로 숙달되어 있는 사람을 고용하는데, 이들을 스턴트맨이라고 부른다.
출처 : https://namu.wiki/w/%EC%8A%A4%ED%84%B4%ED%8A%B8%EB%A7%A8
테스트 더블이란 용어는 xUnit Test Patterns의 저자인 제라드 메스자로스(Gerard Meszaros)가 처음 사용한 용어이다. 테스트 더블은 스턴트 더블에서 유래되었다. 테스트를 위해 시스템의 필요한 부분은 실제 배우가 아닌 스턴트 배우처럼 테스트 더블이란 객체가 실제 테스트에 필요한 객체를 대체하여 테스트를 할 수 있도록 한다.
왜 테스트 더블(Test Double)을 사용하는가?
테스트 더블이 필요한 이유는 명확하다. 각 테스트를 독립적으로 수행하고, 더욱 빠른 결과를 얻기 위함이다. 단위테스트(Unit Test)는 테스트 하려는 대상이 독립적이여야 한다.
안드로이드에서 네트워크를 이용하여 서버에 특정 데이터를 조회하는 모듈 A을 개발하였다고 하자. 테스트 하려는 모듈은 서버와 연결하는 모듈 B와 의존성을 가지고 있을 것이다. 만약 이 모듈 A를 테스트한다면 테스트 시 마다 의존성을 가진 모듈 B가 서버에 연결하고 데이터를 조회할 것 이다. 이 상태가 테스트의 가장 이상적인 상황이라고 생각할 수 있다. 하지만 모듈 A를 테스트하는 경우마다 서버와의 연결과 모듈 B의 무결성을 보장해야 한다. 만약 서버 혹은 모듈 B에 문제가 있는 경우 모듈 A의 테스트에 영향을 줄 것이다.
또한 항시 서버와의 연결은 테스트 속도를 낮춘다. 반복적이고 자주 테스트가 수행되는 시스템에서 이러한 상황은 팀에게 좋은 영향을 주진 않을 것이다. 빠르고 정확한 테스트 결과를 원하기에 항상 서버와의 연결이 된 상태에서 모듈 A를 테스트하길 원하진 않을 것이다. (전체적인 시나리오에 관한 테스트는 별도로 이뤄줘야 한다.)
이러한 경우 서버와 연결하는 모듈 B를 테스트 더블 객체로 대체하여 사용한다. 모듈 B가 실제 배우이고, 테스트 더블 객체가 스턴트 더블이 되는 것이다. 테스트 더블 객체는 실제로 서버와 연결하지도 않을 것이며 실제 데이터를 가져오는 일 따위는 없을 것이다.
테스트 더블(Test Double)의 종류
테스트 더블의 종류는 크게 Dummy, Fake, Sub, Spy, Mock으로 5가지로 나뉠 수 있다.
1. Dummy
- 테스트 더블 중 가장 간단한 구조를 가진다
- 테스트하려는 모듈이 특정 인스턴스는 필요하지만 내부의 기능은 필요하지 않은 경우 사용된다.
- 즉, 내부가 비어져 있을 수 있기에 정상적인 동작을 보장하진 않지만, 테스트에는 영향을 주지 않아야 한다.
즉 Dummy는 테스트에서 해당 인스턴스의 기능은 사용하지 않지만 인스턴스 자체가 필요한 경우에 자주 사용된다. 내부에 필요한 기능은 정상적으로 동작하지 않아도 되는 경우 사용된다. 아래 예시를 보자.
테스트 코드에서 Printer 인터페이스의 객체를 필요로 한다. 하지만 사용하는 인터페이스의 기능은 없다.
@Test
fun PrinterRegisterMatchTypeTest {
val dummyPrinter = DummyPrinter()
val register = IORegister(type = Terminal, printer = dummyPrinter)
assertEqual(register.type, Terminal)
}
DummyPrinter는 Printer 인터페이스를 통해 print() 함수를 재정의한다. 하지만 재정의된 print() 함수는 어떠한 행위도 하지 않고 있으며 이러한 재정의가 테스트 코드에 영향을 주진 않는다.
interface Printer {
void print();
}
class DummyPrinter: Printer {
override fun print() {
// Do nothing
}
}
2. Fake
- 가짜 객체로 진짜 객체처럼 행동해야 하는 테스트더블 객체이다. 즉 내부 동작이 구현이 된 객체이다.
- 복잡한 구현이나 외부 객체들의 동작을 단순화 하여 구현한 객체이다.
테스트 더블이 왜 필요한가? 에서 들었던 예시와 같이, 서버에 직접 연결하여 데이터를 조회하는 것이 아닌, 모듈 B의 Fake 테스트 더블 객체를 사용하여 List, Hash와 같은 여러 자료구조 등을 사용하여 실제로 서버와 데이터를 주고 받으며 정상 동작하는 것 처럼 속이는 가짜 객체이다.
안드로이드에서는 Fake 를 repository 를 이용한 DB, Retrofit 테스트 등에 많이 사용한다.
class FakeItemRepository : ItemRepository {
private val items = mutableListOf<Item>()
private val observableItems = MutableLiveData<List<Item>>(Items)
override suspend fun insertItem(Item: Item) {
Items.add(Item)
}
override suspend fun deleteSItem(Item: Item) {
Items.remove(Item)
}
override fun observeAllItems(): LiveData<List<Item>> {
return observableItems
}
}
3. Stub
- Dummy 에서 구현이 추가된 형태
- 테스트에서 요청하는 데이터를 전달하기 위해 많이 사용
Stub은 미리 정의된 데이터를 저장하고 테스트 중에 호출에 응답하는데 사용하는 테스트 더블 객체이다. Dummy와는 달리 실제로 기능을 필요로 한다. 그래서 대부분의 Stub은 하드코드된 값을 반환하는 함수형태로 된 코드를 많이 확인할 수 있다. 최대한 기능을 단순화하여 제공을 한다.
4. Spy
- 테스트에서 호출한 내용에 대한 데이터 저장 및 활용이 필요한 경우 사용
Spy 테스트 더블 객체는 Stub과 매우 유사한 역할을 하지만, 하나 다른 것은 필요한 정보를 저장하고 있다가 테스트에서 호출 시 활용할 수 있게 한다는 점이 다르다. 예를 들어 테스트에서 특정 데이터를 전달하고, 그 데이터가 유지되고 있는 상태인지를 확인하는 경우 등에서 사용한다.
5. Mock
- 호출에 대한 기대를 명세하고 내용에 따라 동작하도록 프로그래밍 된 객체
Mock 객체는 특정 조건이 발생하면 미리 약속된 행동을 하는 객체이다. 프로덕션 코드를 호출하고 싶지 않거나 쉽게 확인할 수 있는 방법이 없을 때 의도한 코드가 실행되도록 Mock을 사용한다.
Stub과는 다르게 더욱 정교한 테스트를 작성할 수 있다. 만약 Mock을 이용하면 정상적이지 않은 동작에 대해 예외를 던지는 등의 다양한 기능을 사용하여 테스트가 가능하다.
서버에 데이터를 전송하는 서비스를 호출하는 기능을 예로 들어서, 테스트를 실행할 때 마다 서버에 데이터를 전송하고 싶지는 않다. 정상적으로 데이터가 서버로 전송되었는지 확인할 방법도 쉽지 않다. 이 때 할 수 있는 방법은 함수의 결과값을 검증하는 방법밖의 없다.
결론
안드로이드 테스팅에 앞서 테스트 작성 시 사용할 다양한 테스트 더블에 대해 정리를 먼저하였다. 이후에 포스팅할 글에서 좀 더 Dummy, Fake, Stub, Spy, Mock 의 다양한 활용법과 예제를 작성하려 한다.
각 테스트 더블의 구분이 어렵더라면 조금 더 학습하여 명확하게 차이를 정리하고 넘어가는 것을 추천한다.
Reference
'Android > Testing' 카테고리의 다른 글
[Android/Testing] #8. 테스트에 Dagger-Hilt 적용해보기 (0) | 2022.11.08 |
---|---|
[Android/Testing] #7. ViewModel 테스트하기 (with Fake Repository) (0) | 2022.10.22 |
[Android/Testing] #5. 테스트를 위한 기반 프로젝트 생성하기 (0) | 2022.10.02 |
[Android/Testing] #4. Room Database 테스트 (0) | 2022.09.18 |
[Android/Testing] #3. Unit Testing 입문하기 (0) | 2022.09.15 |