[iOS] Test Doubles

Youngwoo Lee·2021년 5월 9일
2

iOS

목록 보기
27/46
post-thumbnail

Test Doubles

Unit Test를 작성하는 동안 production에 사용될 객체와 동일하게 동작하지만 단순화된 버전이 필요한 경우가 있다. 우리는 이런 종류의 객체들을 Test Doubles 이라고 한다. 외부 의존성은 Test Doubles를 통해 관리되며 다양한 시나리오를 쉽게 시뮬레이션할 수 있다

Test Double은 테스트 목적으로 production 객체를 교체하는 모든 객체를 부르는 모든 용어를 말한다. 그리고 이러한 Test Double에는 다섯 가지 종류가 있다

Dummy, Fake, Stub, Spy, Mock
이 Test Doubles들을 알아보기 전에 알아야 하는 개념이 있는데,

상태 기반 테스트 vs 행위 기반 테스트이다


상태 기반 테스트 vs 행위 기반 테스트

상태 기반 테스트(State base test)

  • 특정한 메소드를 거친 후, 객체의 상태에 대해 예상 값과 비교하는 방식이 상태 기반 테스트이다.
func test_add() {
  //given
  let expectation = 10
  //when
  let result = self.calculator.add(4, 6)
  //then
  XCTAssertEqual(result, expectation)
}

행위 기반 테스트(behavior base test)

  • 올바른 로직 수행에 대한 판단의 근거로 특정한 동작의 수행 여부를 이용한다.
  • 메소드의 리턴 값이 없거나 리턴 값을 확인하는 것만으로는 예상대로 동작했음을 보증하기 어려운 경우에 사용
  • 이때 테스트 스파이 객체를 사용하거나 자체적으로 검증 기능을 제공하는 Mock객체를 따로 만들어서 테스트 케이스를 작성하는 것이다.
  • 행위를 점검하는 것으로 테스트 케이스를 만드는 방식
  • 따라서 행위 기반 테스트를 수행할 때는 예상하는 행위들을 미리 시나리오로 만들어놓고 해당 시나리오대로 동작이 발생했는지 여부를 확인하는 것이 핵심이 된다.

ex) methodA에 A가 입력되면 methodB는 호출되지 않아야 정상이다. 그리고 B가 입력되면 methodB가 호출되어야 정상이다. 하지만 대상이 되는 methodA만 놓고 봤을 때 정상 동작 여부를 판단할 수가 없다. 만일 methodA가 동작했을 경우 methodB가 반드시 호출되는 구성이라면, 반대로 methodB의 호출 여부로 methodA의 정상 여부를 판단할 수 있다고 보는 것이다. 따라서 이럴 때는 methodB의 호출 여부를 확인하는 것이 테스트 시나리오의 종료 조건이 된다.



Dummy

Dummy 객체는 사용되지 않을 객체이다. 이것은 Test에 사용되지 않고 placeholder 로만 사용된다. 그리고 Dummy 객체는 메서드 매개변수를 충족하는데도 사용된다.

  • 가장 기본적인 테스트 더블
  • 단지 인스턴트화 된 객체가 필요하고, 해당 객체의 기능까지 필요하지 않은 경우에 사용
  • 해당 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 객체를 만들어준 것이다!


Fake

  • 복잡한 로직이나 객체 내부에서 필요로 하는 다른 외부 객체들의 동작을 단순화하여 구현한 객체이다.
  • 동작의 구현을 가지고 있지만 실제 프로덕트에서는 적합하지 않은 객체이다.

정리하자면, 동작은 실제 사용되는 객체처럼 정교하고, 실제 프로덕트처럼 동작하지 않는 객체를 말한다. 많이들 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
  }
}


Stub

  • 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)
  }
}


Spy

  • Stu의 역할을 가지면서 호출된 내용에 대해 약간의 정보를 기록한다.
  • Test Doubles로 구현된 객체에 자기 자신이 호출되었을 때 확인이 필요한 부분을 기록하도록 구현한다.
  • 실제 객체처럼 동작시킬 수도 있으며, 필요한 부분에 대해서는 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

호출에 대한 기대를 명세하고 내용에 따라 동작하도록 프로그래밍된 객체이다

  1. 테스트 작성을 위한 환경 구축이 어려울 때
    • 환경 구축을 위한 작업 시간이 많이 필요한 경우 Mock 객체를 사용
    • 특정 모듈을 아직 갖고 있지 않아서 테스트 환경을 구축하지 못할 경우
    • 타 부서와의 협의나 정책이 필요한 경우에도 Mock이 필요.
    • 연계 모듈이라서 다른 쪽에서 승인을 해줘야 테스트가 가능한 경우, 방화벽으로 막혀 있어서 통과가 어려운 경우 등이 이에 속함.
  2. 테스트가 특정 경우나 순간에 의존적일 때
  3. 테스트 시간이 오래 걸리는 경우
  • 일반적인 테스트 더블은 상태를 기반으로 테스트 케이스를 작성
  • Mock객체는 행위를 기반으로 테스트 케이스를 작성

참고자료

Unit Testing and Test Doubles in Swift
Swift의 강력한 Mock객체 만들기
What is test-double
Test Doubles

profile
iOS Developer Student

0개의 댓글