Unit Test를 작성하는 동안 production에 사용될 객체와 동일하게 동작하지만 단순화된 버전이 필요한 경우가 있다. 우리는 이런 종류의 객체들을 Test Doubles
이라고 한다. 외부 의존성은 Test Doubles를 통해 관리되며 다양한 시나리오를 쉽게 시뮬레이션할 수 있다
Test Double은 테스트 목적으로 production 객체를 교체하는 모든 객체를 부르는 모든 용어를 말한다. 그리고 이러한 Test Double
에는 다섯 가지 종류가 있다
Dummy, Fake, Stub, Spy, Mock
이 Test Doubles들을 알아보기 전에 알아야 하는 개념이 있는데,
상태 기반 테스트 vs 행위 기반 테스트이다
func test_add() {
//given
let expectation = 10
//when
let result = self.calculator.add(4, 6)
//then
XCTAssertEqual(result, expectation)
}
ex) methodA에 A가 입력되면 methodB는 호출되지 않아야 정상이다. 그리고 B가 입력되면 methodB가 호출되어야 정상이다. 하지만 대상이 되는 methodA만 놓고 봤을 때 정상 동작 여부를 판단할 수가 없다. 만일 methodA가 동작했을 경우 methodB가 반드시 호출되는 구성이라면, 반대로 methodB의 호출 여부로 methodA의 정상 여부를 판단할 수 있다고 보는 것이다. 따라서 이럴 때는 methodB의 호출 여부를 확인하는 것이 테스트 시나리오의 종료 조건이 된다.
Dummy 객체는 사용되지 않을 객체이다. 이것은 Test에 사용되지 않고 placeholder
로만 사용된다. 그리고 Dummy 객체는 메서드 매개변수를 충족하는데도 사용된다.
정리하자면 인스턴스화된 객체가 필요해서 구현된 가짜 객체일 뿐이고, 생성된 Dummy 객체는 정상적인 동작을 보장하지 않는다.
protocol NotificationProvider {
func createNotification()
}
class DummyNotificationProvider: NotificationProvider {
func createNotification() {
fatalError("This is dummy object!!!")
}
}
class Provider {
let notificationProvider: NotificationProvider
init(notificationProvider: NotificationProvider) {
self.notificationProvider = notificationProvider
}
}
let provider = Provider(notificationProvider: DummyNotificationProvider())
//Provider 객체를 테스트 하고 싶지만 init 매개변수를 채워야 하므로 Dummy 객체를 만들어준 것이다!
정리하자면, 동작은 실제 사용되는 객체처럼 정교하고, 실제 프로덕트처럼 동작하지 않는 객체를 말한다. 많이들 In-Memory 형식의 FakeDataBase
를 예시로 든다.
protocol UserRepository {
func save(user: User)
func findUser(by id: Int) -> User
}
final class FakeUserRepository: UserRepository {
var users: [User] = []
func save(user: User) {
if self.findUser(by: user.id) == nil {
users.append(user)
}
}
func findUser(by id: Int) -> User? {
for user in self.users {
if user.id == id {
return user
}
}
return nil
}
}
Dummy
객체가 실제로 동작하는 것처럼 보이게 만들어 놓은 객체를 의미합니다.정리하자면 테스트를 위해 프로그래밍된 내용에 대해서만 준비된 결과를 제공하는 객체이다.
struct Notification {
let id: String
let title: String
}
protocol NotificationGetter {
func getNotification(completion: (([Notification])->Void))
}
class NotificationStub: NotificationGetter {
private let notifications: [Notification]
init(notifications: [Notification] {
self.notifications = notifications
}
func getNotification(completion: (([Notification])->Void)) {
completion(notifications)
}
}
Stub
으로 만들어서 동작을 지정할 수도 있다.정리하자면 실제 객체처럼 동작하고 Stub
객체로도 활용할 수 있으며 필요한 경우에는 특정 메서드가 제대로 호출되었는지 여부를 확인할 수 있는 객체를 Spy
라고 합니다.
class MailingService {
private var sendMailCount = 0;
private var mails: [Mail] = []
func sendMail(mail: Mail) {
sendMailCount += 1
mails.append(mail)
}
func getSendMailCount() {
return sendMailCount;
}
}
호출에 대한 기대를 명세하고 내용에 따라 동작하도록 프로그래밍된 객체이다
- 일반적인 테스트 더블은 상태를 기반으로 테스트 케이스를 작성
- Mock객체는 행위를 기반으로 테스트 케이스를 작성
Unit Testing and Test Doubles in Swift
Swift의 강력한 Mock객체 만들기
What is test-double
Test Doubles