概要
Swiftにおいて、ユニットテストがしやすいよう、テスタブルな設計にする方法の一部として、依存する型をMock化するパターンをまとめます。
パターン一覧
本記事で説明するパターンは以下の4パターンです。
- パターン①:具象クラスに依存、Mock用のサブクラスを作成
- パターン②:抽象クラスに依存、Mock用のサブクラスを作成
- パターン③:ラッパークラスに依存、Mock用のサブクラスを作成
- パターン④:プロトコルに依存、プロトコルに準拠したMock用の具象型を作成
補足
- 下記のコード例ではDIの方法やMockでの振る舞いの変化のさせ方については本旨から逸れるため、特にパターン分けには考慮していません。
- 型のMock化のまとめのため、動的に振る舞いのみを差し替えるMethod Swizzlingなどのパターンも含めていません。
パターン①:具象クラスに依存、Mock用のサブクラスを作成
以下のようにUserDefaultsなど、具象クラスに実装が依存している場合のパターンです。具象クラスを継承したサブクラスを作成することにより、依存クラスをMockに差し替えます。
// 具象クラスに依存、Mock用のサブクラスを作成
struct Pattern1 {
let userDefaults: UserDefaults
init(_ userDefaults: UserDefaults = UserDefaults.standard) {
self.userDefaults = userDefaults
}
}
class MockUserDefaults: UserDefaults { }
let pattern1 = Pattern1(MockUserDefaults())
パターン②:抽象クラスに依存、Mock用のサブクラスを作成
実装的にはパターン①とは大きく変わりませんが、依存クラスが抽象クラス(スーパークラス)の場合のパターンです。抽象クラスを継承したサブクラスを作成することにより、依存クラスをMockに差し替えます。
// 抽象クラスに依存、Mock用のサブクラスを作成
struct Pattern2 {
let apiClient: AbstructAPIClient
init(apiClient: AbstructAPIClient = APIClient()) {
self.apiClient = apiClient
}
}
class AbstructAPIClient { }
class APIClient: AbstructAPIClient { }
class MockAPIClient: AbstructAPIClient { }
let pattern2 = Pattern2(apiClient: MockAPIClient())
パターン③:ラッパークラスに依存、Mock用のサブクラスを作成
以下のようにDateなどの構造体に依存している場合のパターンです。構造体には継承機能がなく、そのままではサブクラスの作成によるMock化はできないため、以下のようなラッパークラスを作成し、ラッパークラスを継承したサブクラスを作成することにより、依存クラスをMockに差し替えます。
// ラッパークラスに依存、Mock用のサブクラスを作成
struct Pattern3 {
let dateWrapper: DateWrapper
init(_ dateWrapper: DateWrapper = DateWrapper()) {
self.dateWrapper = dateWrapper
}
}
class DateWrapper {
var currentDate: Date { return Date() }
}
class MockDateWrapper: DateWrapper {
override var currentDate: Date {
return Date(timeIntervalSince1970: 0)
}
}
let pattern3 = Pattern3(MockDateWrapper())
パターン④:プロトコルに依存、プロトコルに準拠したMock用の具象型を作成
以下のように、プロトコルに実装が依存している場合のパターンです。プロトコルに準拠したMock用の具象型を作成することにより依存型をMockに差し替えます。
※プロトコル、構造体の命名は適当です。
// プロトコルに依存、プロトコルに準拠したMock用の具象型を作成
struct Pattern4 {
let dateWrapper: DateWrapperType
init(dateWrapper: DateWrapperType = DateWrapperImpl()) {
self.dateWrapper = dateWrapper
}
}
protocol DateWrapperType {
var currentDate: Date { get }
}
struct DateWrapperImpl: DateWrapperType {
var currentDate: Date {
return Date()
}
}
struct MockDateWrapperImpl: DateWrapperType {
var currentDate: Date {
return Date(timeIntervalSince1970: 0)
}
}
let pattern4 = Pattern4(dateWrapper: MockDateWrapperImpl())
備考: クラスエクステンションのMock化について
パターン①〜③のようにオーバーライドの機能により振る舞いを変更する場合において、以下のようなextensionに実装されたメソッドの場合は少し工夫が必要です。
class Hoge {
}
extension Hoge {
func fuga() {
print("not mock")
}
}
class HogeMock: Hoge {
// コンパイルエラー
// Overriding non-@objc declarations from extensions is not supported
override func fuga() {
print("mock")
}
}
このままビルドすると、override部分で以下のコンパイルエラーとなります。
Overriding non-@objc declarations from extensions is not supported
少し対応がわかりにくいエラーですが、継承元のメソッドに@objc
属性を付与することで、オーバーライドできるようになります。
extension Hoge {
@objc func fuga() {
print("not mock")
}
}