반응형
테스트 대상 객체가 다른 객체와 의존성이 있는 경우 이런 경우 실제 구현체 대신 해당 객체의 동작을 모방하는 객체를 만들어 테스트에 영향이 없도록 만들어야 한다.
이 때 의존성이 있는 객체의 모방하는 객체를 Test Double라 부른다.
Test Doubles
Test Doubles는 외국에서는 스턴트맨을 Stunt Double이라고 부르는데 여기서 따온 말이다.스턴트맨이 배우를 대신 하는 것처럼 Test Doubles도 실제 객체를 대신해 동작하기 때문
Test Doubles의 종류
- Dummy
- Stub
- Fake
- Spy
- Mock
Test Doubles 예시
class UserProfileFetcher(
private val userRepository: UserRepository,
private val contactRepository: ContactRepository
) {
fun getUserProfileById(id: String): UserProfile {
return UserProfile(
id = id,
name = userRepository.getNameByUserId(id),
phoneNumber = contactRepository.getPhoneNumberByUserId(id)
)
}`
}
interface UserRepository {
fun saveUserName(id: String, userName: String)
fun getNameByUserId(id: String): String
}
interface ContactRepository {
fun getPhoneNumberByUserId(id: String): String
}
data class UserProfile(val id: String, val name: String, val phoneNumber: String)
Dummy
Dummy 는 아무런 동작도 하지 않는 객체
구현 자체가 없으며, 반환이 필요한 값에는 빈 값을 반환
class DummyContactRepository : ContactRepository {
override fun getPhoneNumberByUserId(id: String): String {
return ""
}
}
Stub
Stub은 더미와 비슷하지만, 실제 데이터를 반환하는 객체
class StubContactRepository : ContactRepository {
override fun getPhoneNumberByUserId(id: String): String {
return "010-xxxx-xxxx"
}
}
Fake
Fake는 실제처럼 동작하는 모방 객체
간단한 구현이 들어가 있어 실제처럼 동작하도록 만드는 객체
class FakeUserRepository : UserRepository {
private val userNameMap = mutableMapOf<String,String>()
override fun saveUserName(id: String, userName: String) {
userNameMap[id] = userName
}
override fun getNameByUserId(id: String): String {
return userNameMap[id] ?: throw IllegalArgumentException("User Id가 존재하지 않습니다.")
}
}
이 UserRepository는 영속성 있는 데이터베이스에 저장되지는 않지만 인메모리에서 실제 데이터베이스에 저장되는 것과 비슷하게 동작한다.
Spy
Spy는 객체와의 상호작용을 기록하는 모방 객체
메소드가 실행되었는지 확인이 가능하다.
class SpyUserRepository : UserRepository {
private val userNameMap = mutableMapOf<String, String>()
private var isGetNameByUserIdCalled = false
override fun saveUserName(id: String, userName: String) {
userNameMap[id] = userName
}
override fun getNameByUserId(id: String): String {
isGetNameByUserIdCalled = true
return userNameMap[id] ?: throw IllegalArgumentException("User Id가 존재하지 않습니다.")
}
fun verifyGetUserNameByUserByIdCalled() {
if(!isGetNameByUserIdCalled) {
assert(false)
}
}
}
Mock
Mock은 Spy보다 조금 더 정확한 상호작용을 기록
어떤 함수에 대한 입력과 출력이 있었는지 기록
class MockUserRepository : UserRepository {
private val userNameMap = mutableMapOf<String, String>()
private val getNameByUserIdCallHistory: MutableList<GetNameByUserIdCallHistory> = mutableListOf()
override fun saveUserName(id: String, userName: String) {
userNameMap[id] = userName
}
override fun getNameByUserId(id: String): String {
val result = userNameMap[id] ?: throw IllegalArgumentException("User Id가 존재하지 않습니다.")
getNameByUserIdCallHistory.add(GetNameByUserIdCallHistory(id, result))
return result
}
private data class GetNameByUserIdCallHistory(val inputId: String, val returnValue: String)
}
테스트 마다 매번 직접 Test Double를 만들면 테스트 마다 많은 코드가 생성이 된다.
Test Double를 직접 만드는 경우는 많지 않으며 대부분 Mockito나 Mockk 같은 라이브러리를 사용해 코드의 절대량을 줄인다.
긴 글 읽어 주셔서 감사합니다.
반응형
'CS' 카테고리의 다른 글
[CS] 클린 코드 책 요약 (3) | 2024.12.27 |
---|---|
[CS] CORS는 무엇일까? (0) | 2024.12.26 |
[OAUTH] OAuth 2.1 (2) | 2024.12.14 |
[CS] JOSE (1) | 2024.12.11 |
[CS] 개발 표기법 정리 (0) | 2020.09.02 |