Unit Test 5장의 내용을 정리한 글입니다.
스턴트 대역 (Stunt Double)에서 파생한 말로, 객체를 테스트 하는 상황에서, 의존하는 객체들을 대신해 동작하는
대역 객체들을 일컫는 말입니다.
Stub
으로는 상호작용을 검증하지 말자과잉 명세 (overspecification)
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mockito
import org.mockito.Mockito.`when`
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.*
@ExtendWith(MockitoExtension::class)
internal class StubTest {
@Test
fun `Do not verify Stub method call`() {
// given
val stub =mock<IDB>()
whenever(stub.getNumberOfUsers()).thenReturn(10)
// when
val sut = Controller(stub)
val result = sut.createReport()
// then
assertEquals(Report(10),result)
// BAD LINE!!
verify(stub,times(1)).getNumberOfUsers()
}
}
interface IDB {
fun getNumberOfUsers(): Int
}
class Controller(
private val db: IDB
) {
fun createReport(): Report {
return Report(db.getNumberOfUsers())
}
}
data class Report(val id: Int = 0)
우리는 식별할 수 있는 동작
에 관심을 가지고, 구현 세부사항
을 이들과 분리해야 합니다.
그래야만 리팩토링 내성을 기르고, 변경에 유연한 코드를 작성할 수 있습니다.
그렇다면 이 둘을 어떻게 구분해낼 수 있을까요?
Operation
을 노출 → 계산을 수행하거나, 부작용을 초래함State
를 노출 → 시스템의 현재 상태갑자기 이 소리를 하는 이유가 뭐냐면..
식별할 수 있는 동작 | 구현 세부 사항 | |
---|---|---|
공개 | 좋음 | 나쁨 |
비공개 | 비공개일 수가 없음 | 좋음 |
캡슐화를 잘 지키고, 리팩토링 내성 (변경사항 내성)을 잘 지킨다..
클린 아키텍처에서 살펴본 내용이랑 굉장히 닮아 있습니다.
책에서도 Hexagonal Architecture를 언급하고 있네요 :)
(헥사고날 아키텍처에 대한 너무 좋은 출처가 있어 공유해봅니다.)
Mock
을 쓰면 될까요?의존성에는 두 가지 종류가 있습니다.
공유 의존성 (Mutable)
비공유 의존성
공유 의존성은 다시 두 가지로 나뉩니다.
테스트 격리의 측면에서, 런던파는 모든 공유 의존성을, 고전파는 "프로세스 외부의" 공유 의존성을 mocking
합니다.
그러나 두 분파 모두 리팩토링 내성
의 측면에서는 관리가 힘든 (brittle) 테스트를 만들어낼 위험이 있습니다.
프로세스 외부에 있다고 해서, Mock
을 무조건 써야 하는 것은 아닙니다.
만약 클라이언트가 접근을 못하는, 오직 앱만 접근 가능한 의존성이라면,
=> 이것은 구현 세부 사항
이지 식별 가능한 동작(계약)
이 아닙니다.
즉 언제든 갈아끼워질 준비가 되어있어야 합니다.
Mocking은 기본적으로 테스트에 구현 세부 사항을 노출하는 행위이기 때문에,
이러한 구현 세부 사항에 대해서 과도하게 Mocking을 하는 행위는 좋지 않습니다.
💡 결론: 시스템 간 통신 (앱 경계를 넘는 통신)과 해당 통신의 부작용이 외부 환경에서 클라이언트에게 보일 때 Mock을 사용하자!
reference:
when-to-mock (Unit Testing 저자 블로그)