23
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Swiftの型のMock化パターンまとめ

Last updated at Posted at 2018-04-17

概要

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")
    }
}
23
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?