Test Double이란 무엇인가?
다른 객체에 의존성이 있는 객체들은 테스트 하기 어렵다. 테스트 시 만약 의존성이 있는 객체의 진짜 인스턴스를 사용하면 이 의존성이 있는 객체에 테스트가 영향을 받게 된다. 이를 해결하기 위해 가짜 객체를 사용하는데 이를 Test Double이라 한다.
Test Double의 종류
Test Double에는 Fake(가짜), Stub(스텁), Mock(목) 3가지 종류가 있다.
Fake
Fake는 테스트를 위한 가짜 구현을 하는 방식이다. 예를 들어 DB에 저장하는 코드를 가진 클래스가 있다고 하면 이를 메모리에 대신 저장하는 방식으로 가짜 구현을 한다. 즉, 프로덕션의 구현에 영향을 받지 않고 구현을 하는 것이다.
예를 들어 다음과 같이 데이터 레이어에서 Todo를 데이터베이스에 넣거나 메모리에 캐싱하는 역할을 하는 TodoRepository, 데이터베이스 접근을 위한 객체인 TodoDao, 모델 클래스인 Todo 클래스가 있다고 해보자.
class TodoRepository(private val todoDao: TodoDao) {
fun insertTodo(todo: Todo) {
todoDao.insertTodo(todo)
}
fun getTodo(id: String): Todo? {
return todoDao.getTodo(id)
}
}
interface TodoDao {
fun getTodo(id: String): Todo?
fun insertTodo(todo: Todo)
}
data class Todo(val id: String, val title: String)
여기서 TodoRepository를 테스트 하려면 실제 데이터베이스에 접근하는 TodoDao 객체 대신 가짜 객체가 필요하다. 따라서 아래와 같이 비슷하게 동작하는 TodoDaoFake 객체를 만들 수 있다.
class TodoDaoFake : TodoDao {
private val fakeDatabase: MutableMap<String, Todo> = mutableMapOf()
override fun getTodo(id: String): Todo? {
return fakeDatabase[id]
}
override fun insertTodo(todo: Todo) {
fakeDatabase[todo.id] = todo
}
}
그러면 이제 이 Repository에 대한 테스트를 다음과 같이 할 수 있다.
class TodoRepositoryFakeTest {
@Test
fun testInsert() {
val todoRepository = TodoRepository(TodoDaoFake())
val insertedTodo = Todo("id", "todo")
todoRepository.insertTodo(insertedTodo)
val fetchedTodo = todoRepository.getTodo("id")
Assert.assertNotNull(fetchedTodo)
Assert.assertEquals(fetchedTodo, insertedTodo)
}
}
Stub
Stub은 미리 정의된 데이터를 반환한다. 즉, 테스트를 할 때 요청에 대한 응답을 정해놓는다.
예를 들어 앞선 글에서 다룬 RandomStringGenerator에서 RandomCharacterFactory가 '가'를 무조건 반환하도록 만들면 이게 바로 Stub이다.
@Before
fun setUp(){
randomStringGenerator = RandomStringGenerator(
// Stub
object : RandomCharacterFactory {
override fun get(): Char {
return '가'
}
}
)
}
그러면 테스트를 할 때 아래와 같이 해당 값이 제대로 반환되는지만 확인하면 된다.
@Test
fun test() {
val result = randomStringGenerator.generate(5)
assertEquals(result.length, 5)
assertEquals(result, "가가가가가")
}
전체 코드는 다음과 같다.
internal class RandomStringGeneratorStubTest {
lateinit var randomStringGenerator: RandomStringGenerator
@Before
fun setUp(){
randomStringGenerator = RandomStringGenerator(
// Stub
object : RandomCharacterFactory {
override fun get(): Char {
return '가'
}
}
)
}
@Test
fun test() {
val result = randomStringGenerator.generate(5)
assertEquals(result.length, 5)
assertEquals(result, "가가가가가")
}
}
Mock
Mock은 테스트 중의 interaction을 기록한다. 위에서 Stub의 코드에서 interaction이 일어나는 것을 기록하는 것이 바로 Mock 객체이다.
예를 들어 get이 불린 횟수를 기록해두는 객체인 getCallCount를 만들고 해당 객체를 get이 불릴 때마다 증가시키는 방식으로 interaction을 기록할 수 있다. 이를 getCallCount() 메서드를 통해 가져올 수 있도록 한다.
즉, 다음과 같이 RandomCharacterFactory를 Mock 객체로 구현할 수 있다.
class RandomCharacterFactoryMock() : RandomCharacterFactory {
private var getCallCount = 0
override fun get(): Char {
getCallCount++
return '가'
}
fun getCallCount() = getCallCount
}
그러면 위에서 만든 Mock 객체는 다음과 같이 테스트에 사용이 가능하다. RandomStringGenerator가 길이 5짜리 String을 만들었을 때 RandomCharacterFactory는 5개의 Character을 생성해야 하므로 5번 불려야 한다.
internal class RandomStringGeneratorMockTest {
lateinit var randomStringGenerator: RandomStringGenerator
lateinit var randomCharacterFactory: RandomCharacterFactoryMock
@Before
fun setUp() {
randomCharacterFactory = RandomCharacterFactoryMock()
randomStringGenerator = RandomStringGenerator(randomCharacterFactory)
}
@Test
fun test() {
val result = randomStringGenerator.generate(5)
//Test Mock
assertEquals(randomCharacterFactory.getCallCount(), 5)
}
class RandomCharacterFactoryMock() : RandomCharacterFactory {
private var getCallCount = 0
override fun get(): Char {
getCallCount++
return '가'
}
fun getCallCount() = getCallCount
}
}
Mock과 Test Double
위에서 Test Double에는 Mock이라는 타입이 존재하는 것을 알아보았다.
하지만 보통 테스트를 위해 Test Double을 만들어주는 라이브러리를 Mocking Library라고 부르기 때문에 Mock을 Test Double로 지칭하는 개발자들이 많다. Test Double과 Mock은 같은 의미로도 쓰일 수 있다는 것을 명심하자.
정리
위의 코드들을 보면서 의존성이 있는 객체가 하나 뿐인데도 복잡하다고 느꼈을 것이다. 이러한 Test Double들을 만들기 위해서 다양한 라이브러리들이 존재한다. 다음 시간에는 Mockito 라 불리는 라이브러리를 사용해 Test Double을 만들고 테스트를 진행해볼 것이다.
'Unit Testing' 카테고리의 다른 글
[Mockito] when 사용법 한 번에 정리하기 : thenReturn, thenAnswer, doThrow (0) | 2022.12.20 |
---|---|
[Unit Testing] Mockito 사용해 Test Double 만들기 (0) | 2022.12.19 |
Unit Testing에서 Test Double이 필요한 이유는 무엇일까? (0) | 2022.12.17 |
Kotlin에서 사용할 수 있는 JUnit assert 종류 알아보기 : assertEquals, assertTrue, assertThrows, assertNotNull (0) | 2022.12.16 |
IntelliJ, Android Studio에서 Test Coverage 확인과 Test Coverage의 한계점 (0) | 2022.12.15 |