Tryswift veronica ray cover

Swiftにおける現実的なモック

モックを使うことでプロダクション環境のデータを壊すことなく素早くテストを書くことができます。OCMockを使わずに、あまり手間をかけずにモックを書くにはどうしたらいいでしょうか。try! Swiftで話されたこの講演で、Veronica RayはSwiftにおける実践的でメンテナンスしやすくシンプルなモックを書く方法を示します。


テストは書きたいけれど… (0:11)

…プロダクション環境のデータを壊してしまったり、テストが遅かったりするのは困ります。こちらはとても簡単なNSURLsessionを使う例です。みなさんのコードにもあるかと思います。もし実際の通信と同じリクエストを使ってテストをしたとすると、テストはすごく遅くなってしまいます。非同期のシステムに対するテストはあまり信頼性がありません。たくさんテストを追加するといろいろなところがランダムに壊れたりしてしまいます。

let session = NSURLSession()
let url = NSURL(string: "http://www.tryswiftconf.com")!
let task = session.dataTaskWithURL(url) { (data, _, _) -> Void in
    if let data = data {
        let string = String(data: data, encoding: NSUTF8StringEncoding)
        print(string)
    }
}
task.resume()

それでは、実際のリクエストを使わずにどうやってテストしたらいいのでしょうか?

OCMockには制限があります (1:03)

技術的にはSwiftでもOCMockを使うことができます。しかし、大きな制限が伴い、機能的には大変に限定されたものになりますので、これから使おうとする必要はありません。モックフレームワークはリフレクションを活用して実行時にクラスやオブジェクトの型を変えることを利用しているためです。

Swiftのリクフレクションは読み込み専用なので、実行時にプログラムの挙動を変えることはできません。これはSwiftが安全なプログラミング言語であるということでもありますので、この挙動は簡単には変���ることはないと思います。Swiftにはいくつかのモック化をサポートするフレームワークがありますが、それらはOCMockのような言語のランタイムライブラリにアクセスして実行時に挙動を変えるというタイプのものではありません。

要するに、Swiftでは自分でモックを書く必要があります。

class HTTPClientTests: XCTestCase {
    var subject: HTTPClient!
    let session = MockURLSession()

    override func setUp() {
        super.setUp()
        subject = HTTPClient(session: session)
    }
class HTTPClientTests: XCTestCase {
    var subject: HTTPClient!
    let session = MockURLSession()

    override func setUp() {
        super.setUp()
        subject = HTTPClient(session: session)
    }

    func test_GET_RequestsTheURL() {
        let url = NSURL(string: "http://www.tryswiftconf.com")!

        subject.get(url) { (_, _) -> Void in }
class HTTPClientTests: XCTestCase {
    var subject: HTTPClient!
    let session = MockURLSession()

    override func setUp() {
        super.setUp()
        subject = HTTPClient(session: session)
    }

    func test_GET_RequestsTheURL() {
        let url = NSURL(string: "http://www.tryswiftconf.com")!

        subject.get(url) { (_, _) -> Void in }

        XCTAssert(session.lastURL === url)
    }
}

記事の更新情報を受け取る

ということで上記のようにNSURLSession()のモックを作り、使っていきます。getメソッドを特定のURLに対して呼び、最終的にNSURLSessionのオブジェクトが受け取ったURLが渡したURLと同じであることを確認しています。

時間がかかりそう (2:46)

OCMockが自動的にモックを作成してくれることに比べると、自分でモックを書くことはより時間のかかることに思えます。もっと効率的な方法をお見せします。しかしそれは本当にやるべき価値があるのでしょうか?モックを書くときはいつでも、実際のオブジェクトを使ってテストをするときと比べて、モックを書くために費やす時間や、モックを使うようにテストを統合する時間、書いたモックをメンテナンスする時間などと見合うものかどうかを常に考えなければなりません。これから、そのトレードオフをどう考えたらいいかをお見せします。また、本当に書く必要があるモックだけを書くべきです。

どうしてモックを使うのでしょうか? (3:35)

  • テストを(時には数千倍も)速くするため 🚄

ネットワークを超えてアクセスされるウェブサーバやデータベース、サービスの処理はローカルのコンピュータ内の処理に比べると数千倍も時間がかかります。そのため、ネットワークを使うテストは遅くなります。遅いテストは頻繁に実行されなくなってしまうので、価値が下がってしまいます。

  • テストカバレッジを増加させるため 🌎

一部のエラーや例外をテストすることは難しい場合がありますが、モックを使ってエラーをシミュレートすることで可能になります。ファイルを消したり、データベースをクリアするような処理はプロダクション環境でテストするのは大変危険なので、モックを使わない限り安全にテストすることは難しいです。

  • テストを堅牢にするため 💪

モックを使っていなければ、システムが障害が起こっていたりなどすると、テストは失敗します。ネットワーク通信のタイミングによって予期しない読み込みが発生したり、データベースに予期しない行が増えていたり、あるいは消えていることもあります。設定ファイルが知らない間に変更されているかもしれないということも、あり得ないことではありません。

テストを書きましょう (4:56)

テストはiOS開発ではあまり普及してないかもしれませんが、テストを書くことはとても重要ですし、今からでもやるべきです。エドモンド・ラウ著「The Effective Engineer」は私に非常に大きな影響を与えました。テストを追加することが、コードベースを改善するための最高にレバレッジを効かせた方法であると確信しています。テストがあることでコードに変更を加えることができます。特に大きなリファクタリングをするときでも大きな自信を持って行うことができます。もしネットワーク通信のやり方を変えようとするときや、並列処理を改善するためにReactiveCocoaを試してみようとするとき、何も壊していないことを保証するためにテストが欲しいと思います。アプリケーションを改善しようとするとき、最初に見るべきものの一つはテストです。テストは不具合や手作業のテストにおける繰り返し作業を減らします。コードを壊してしまったときには、自動化されたテストがあると、何が問題だったのか見つけることが容易になります。テストはまた実行可能なドキュメントでもあります。

今こそテストを書き始めるときです

そのコードを書いた人が内容についてよく覚えているうちにテストを書くのがもっとも簡単です。何年も経ってから誰かが変更しようとするときにテストを書くのはそれに比べるととても難しいです。また、テストを書く文化を育むのはとても時間と努力が必要なので、今やり始めるのがベストです。

依存性注入(DI) (6:44)

これは複数のコンストラクタインジェクションとデフォルトコンストラクタです。デフォルトコンストラクタは利便性のために用意されていて、引数を渡すことなしに、すべてのオブジェクトを初期化することができます。この例はJavaプログラマの間ではバッドプラクティスとされています(Bastard Injectionというアンチパターンとして知られています)。しかし、DIコンテナを使用するなら、問題の原因となりますが、自分でオブジェクトを注入、またはフレームワーク(例、TyphoonやSwinject)を介して行う場合はバッドプラクティスにはなりません。

class VideoCaptureManager: NSObject {
    var userDefaults: UserDefaultsProtocol

    //MARK: - Initializers
    override init() {
        self.userDefaults = UserDefaults()
    }

    convenience init(userDefaults: UserDefaultsProtocol) {
        self.init()
        self.userDefaults = userDefaults
    }
}

“依存性注入とはインスタンス変数にオブジェクトを与えるということです。本当にただそれだけです。” - James Shore

シングルトンを使えばいい? (7:58)

こちらはVideoUploadManagerを1行でシングルトン化した例です。このオブジェクトはTimeMachineAPIによってビデオをアップロードするときに使われます。

class VideoUploadManager {
    static let sharedInstance = VideoUploadManager()
}

class TimeMachineAPI {
    func uploadVideo(videoRecording: VideoRecording) {
        VideoUploadManager.sharedInstance.upload(videoRecording: self.videoRecording, completion: { (error) -> Void in
            if let error = error {
                Log.error(error.localizedDescription)
            }
        })
    }
}

なぜDIを使うのでしょうか? (8:19)

  • カスタマイズしやすい 🎨

オブジェクトを作るときに特定の要件に応じてカスタマイズをしやすくなります。同じシングルトンオブジェクトをいろいろなところで使い回すよりも良いです。

  • オーナーシップが明確に 🔐

特にコンストラクタインジェクションを使う場合に、オブジェクトのオーナーシップのルールが強制されることになるので、循環参照が避けやすくなります。

  • テストしやすい 😍

アクセスできないオブジェクトを管理する必要がなくなるので、オブジェクトをモック化することが簡単になり、テストする対象だけにフォーカスすることができます。

コンストラクタインジェクション (9:14)

依存性注入にはいくつかの方法が知られていますが、コンストラクタインジェクションが一般的に好まれています。というのも、依存関係が明確になるからです。これから私が用いる例ではすべてコンストラクタインジェクションを用いることにします。

Test Doubles (9:31)

DIを使うことで、本当のオブジェクトの代わりに テストダブル (モックやスタブのこと)を渡すことができるようになります。OCMockはスタブや、モック、あるいはパーシャル(部分的)モックを簡単に作ることができます。たくさんのテストに関する資料を見てきて、これらの言葉の定義がiOSとJavaの間できちんと一貫して使われていることがわかりました。これはとても重要なことです。なぜならJavaはこの種のテストやモックに関する話題についてはよく研究されていて、そもそもモックは初めはJavaのコミュニティで開発されたものなので、この分野では最も権威のあるプログラミング言語だからです。

テストダブルの種類:

  • スタブ
  • モック
  • パーシャルモック

スタブ (10:08)

“オブジェクトのメソッド呼び出しをニセの戻り値に置き換えます。” - Unit Testing Tutorial: Mocking Objects

スタブでもっともよくあるのはAPIをスタブするものでしょう。APIのスタブは本物のAPIを叩く代わりに、ニセのレスポンスを返します。スタブはAPIがまだできてないときに、APIに依存した部分を開発する際にも有用です。スタブは下記のようになります。このAPIはTimeMachineAPIを呼び出し、引数として与えた年数を遡るタイムマシンを作り出します。そしてそこで見てきたショートビデオを撮影し、現代に戻ってきたときにYouTubeにアップロードします。スタブAPIは実際のAPIを呼び出す代わりにハードコードされたURLを単に返します。

class StubTimeMachineAPI: TimeMachineAPI {
    var videoUrl = "https://www.youtube.com/watch?v=SQ8aRKG9660"

    func getVideoFor(year: Int) -> String {
        return videoUrl
    }
}

モック (11:18)

“メソッドが呼び出されるか、あるいはプロパティがセットされるかチェックしましょう。” - Unit Testing Tutorial: Mocking Objects.

モックの場合はもう少し複雑です。TimeMachineは任意の年数を遡ることができます。MockTimeMachinetimeTravelWasCalledというメンバ変数を持っています。タイムトラベルをするためのメソッドを呼び出すと、timeTravelWasCalledtrueに設定されます。このことにより、このメソッドが確実に呼ばれたことをテストすることができます。

class MockTimeMachine: TimeMachine {
    var timeTravelWasCalled = false

    mutating func timeTravelTo(year: Int) {
        timeTravelWasCalled = true
    }
}

パーシャルモック (12:07)

“いくつかのメソッドが人工的に作られた戻り値を返すように、実際のオブジェクトをラップするか変更します。それ以外は変更を加えません。” - Justin Searls

パーシャルモックはアンチパターンです。これを使うことはおすすめしません。またなぜそれが悪いものなのかの詳細もここでは触れることはしません。

軽い気持ちでアンチパターンという言葉を持ってきたわけではありません。まず、パーシャルモックはセットアップが大変です。実際のオブジェクトをインスタンス化する必要があります。そして、それをラップしてモックとして使います。その次にテストが何をしているのか理解するのが難しくなります。😶

パーシャルモックを使うと、このような質問を受けることが良くあります。(例)「テストしている値はどれですか?これは本当の値ですか?それともニセの値ですか?テストが通ったということは、実際の環境でも同じように動くと考えていいのですか?」このようなコメントをチームメイトからもらって、それでも私はこのテストが効果がないとは思いませんが、そうでなくても、もっと効果的なテストを書いた方が良さそうです。

テストダブルについての詳細はあまり触れませんが、この講演ではモックやスタブのことをテストダブルと呼んでいます。モックだけを指すわけではありません。

プロトコルを用いてモック化する (14:05)

プロトコルを用いてモック化する、という方法はベストプラクティスだと思います。プロトコルをモックに使おうとはあまり考えなかったと思います。2015年のWWDCのセッション、Protocol-Oriented Programming in SwiftでDave Abrahams氏はこのように述べています。 このようなテストの方法はモックを使った方法に似ていますが、プロトコル志向プログラミングの方がより良いと思います。 また、 モックは本質的に壊れやすい。テストコードと密接に結合しなければならず、(中略)実装の詳細がテストに露出する とも述べています。

このセッションからプロトコルを利用することはサブクラス化に比べて多くの利点があることがわかりました。この利点はモックを書くときにも応用することができます。別の良い名前が登場するまでテストダブルのことを「モック」「スタブ」という言葉を使って説明していくことします。他のこの話題についての資料もほぼ同じアプローチをとっています。

プロトコルを用いてモック化することがサブクラス化よりも利点があることをご説明します。まず、 Structとクラスで使える ということです。つまり、コードベースにおいてモック化に一貫した方法が使えるということです。

プロトコルはまた、 内部的な依存がある ときに有用です。NSURLSessionでは、プライベートなクラスとやり取りが発生します。テストをするために、1つか2つのクラスをモック化する必要があるでしょう。内部で使われているプライベートなクラスなので、iOS 9がリリースされたときに名前が変わるかもしれません。もしくは、新しい依存が追加されてそれもスタブ化する必要が出てくるかもしれません。もしスーパークラスが何らかのプロパティを保持していたら、それを受け入れる必要があります。しかし、プロトコルに準拠していたら、このようなことに悩む必要はなくなります。もっとたくさんのベストプラクティスがあります。ほとんどはJava由来のものです。

“自分のものでない型をモック化するな” (16:26)

ではこれからSwiftを用いてiOSでどのように良いプラクティスが応用できるのか見ていきましょう。1999年にモックを開発したロンドンのThoughtWorksの方々は、「 自分のものでない型をモック化するな 」と言っています。

自分のものでない型をモック化してはいけない理由は主に2つあります。

  • 外部のライブラリの振る舞いがモックと同じであることを保証する必要が出てきます。つまり、外部のライブラリを、それがオープンソースであってもソースコードが公開されてないとしても、よく知る必要が出てきます。

  • 外部ライブラリは変わることがありますし、そうするとモックが壊れます。���去の経験から、ライブラリがどれだけ安定しているか、壊れやすいかはわかるかもしれませんが、自分のコードほどは正確に理解できないと思います。

しかし、私たちiOSデベロッパーはアップルのフレームワークが提供しているクラスをモック化する必要があります。

アップルのフレームワークのクラスをモック化する (17:43)

class UserDefaultsMock: UserDefaultsProtocol {

    private var objectData = [String : AnyObject]()

    func objectForKey(key: UserDefaultsKey) -> AnyObject? {
        return objectData[key.name]
    }     

    func setObject(value: AnyObject?, forKey key: UserDefaultsKey) {
        objectData[key.name] = value
    }

    func removeObjectForKey(key: UserDefaultsKey) {
        objectData[key.name] = nil
    }
}

ときにはそれらをモック化する必要があります。それはセットアップが難しいときや、他のテストに影響を与えるときです。

このような場合に良く例に出てくる2つの主なクラスはNSUserDefaultsNSNotificationCenterです。このコードはNSUserDefaultsをモック化したものです。NSNotificationCenterについてはこの後でもう少し詳しくお話しします。結論としてはNSNotificationCenterをモック化することはあまり役には立ちませんでした。

2つ、アップルのクラスをモック化する際に良いアドバイスを聞きました。モック化の際にはそのクラスとのやり取りだけ、振る舞いを含めてはいけないといいます。実装の詳細はいつ変わるかわからないからです。NSUserDefaultsは15年間変わっていませんので、おそらく将来に渡っても大きな変更がなさそうだと思います。

このくらいのリスクであれば、誰かのNSUserDefaultsの本当のデータを間違って壊してしまうことと比べれば、リスクを取る価値はあるだろうと思います。

UserDefaultsMockを使ってテストする (19:05)

例えば、タイムマシンによって最新の開発状況を保つことができるというアプリを考えます。このアプリをTime Traveler Timesと呼ぶことにします。これはNSUserDefaultsのテストです。最新の変更、または毎日のダイジェストをプッシュ通知で受けるというのをやめたい場合は、データプロバイダの返す通知の設定はそれを反映したものにしなければなりません。

func testNotificationSettingsLoad() {
    let userDefaultsMock = UserDefaultsMock()
    mockUserDefaults.setObject("NEVER", forKey: "timeMachineBreakingNewsPushNotification") mockUserDefaults.setObject("NEVER", forKey: "timeMachineDailyDigestPushNotification")

    let dataProvider = PushNotificationsDataProvider(userDefaults: mockUserDefaults)

    let expectedFrequencies: [PushNotificationFrequency] = [.Never, .All, .All, .Never]

    XCTAssertEqual(expectedFrequencies, dataProvider.notificationSettings.values)
}

NSNotificationCenterをモック化する (19:21)

私のプロジェクトにおいてはNSNotificationCenterのモック化はあまり役に立ちませんでした。

  • モック自体がとても複雑になってしまいました。これを書くのに数日かかるほどでした。
  • コードベースの本当に広い範囲で利用されていたので、モックで置き換えるのに少し作業が必要でした。
  • 実際のオブジェクトの振る舞いを反映したモックを作るのが難しかったです。コンパイルを通すために美しくないハックに頼りましたが、そのためにモックの利便性が下がってしまいました。
  • NSNotificationCenterをモック化することでテストの最中に実際の通知が送られてしまうことは避けることができました。しかし、他のテストに影響を与えないことには役に立ちましたが、NSNotificationCenterをモック化することでは問題は解決しないと思いました。

値型をモック化しないように (20:24)

この考え方はすでにSwiftコミュニティに受け入れられてきました。シンプルなデータをやり取りしているだけなら、モックやスタブを使う必要はありません。モックを減らす方法の一つはもっと値型を使うことです。値型を使うこことはモックを作成する時間と将来的にモックをメンテナンスにかかる時間をなくします。

コードベースのあらゆるところでクラスを使用している場合は、いきなり値型に置き換えることは難しいと思います。値型に置き換えていくために、大きな変更を既存のコードベースに加えるのは不安があります。安心してください。コードベースを丸ごと書き換えなくても済む方法があります。

イミュータブルな参照型を使いましょう (21:33)

値型と参照型のセマンティクスについてエキスパートである必要はありません。Swiftの開発者であるJoe Groffは徐々に参照型から値型にリファクタリングするための方法についてとても良いアドバイスをしています。「リファレンス型をイミュータブルにしなさい」と。

イミュータブルなクラスとStructはほとんど同じように振る舞います。クラスをイミュータブルにすることができたなら、今度はそれをStructにすることは簡単です。まずコードベースでクラスが使われているところをすべてあらいだします。そして、「これはクラスである必要があるだろうか」と問いかけます。中には本当に参照型のセマンティクスが必要なケースもあるでしょう。もしくは、NSObjectを継承する必要があったのかもしれません。しかし、そうではないところもあるはずです。なぜそのように書かれているのかを学び取り、改善点を見つけていきましょう。

良いモックとは何でしょうか (22:35)

  • 早く、簡単に書くことができる

どれをモックにするか、どういうアプローチでモック化するべきか、ということを考えるのは、時間がかかることですが、それでもモックを作るのに3日間まるまるかかっている場合は、それが本当に時間に見合う価値があるのか考え直してみましょう。

  • 比較的短く、必要のない情報は含まれていない

もし自分のものでない型のモックを作ろうとしているなら、自分のコードで使っていないメソッドはたとえオリジナルにあるものでも、含めるべきではありません。またプロパティについても、必要なものだけにする方がいいでしょう。

  • 実際のオブジェクトを使わない場合の正当な理由

モックを書き、メンテナンスし続けるためのコストと、速く、安定したテストができることを天秤にかけましょう。私は、何をモック化するのか、どうしてモック化するのかということを徹底的に考えています。

Swiftのモックフレームワーク (24:07)

みなさん、未来を見ていきましょう。Swiftにおけるモックフレームワークはどのようなものになるでしょうか?こちらの3つのフレームワークはどれも似たようなアプローチをとっています。

  • Dobby、MockFive、SwiftMock

この3つのフレームワークのアプローチでは、自分でモックを書く必要があります。ただし、フレームワークが戻り値をスタブ化することと、期待値を設定することのヘルパーを提供してくれます。

  • Cuckoo

こちらは上記のライブラリとは少し異なるアプローチのものです。クラスやStructに対応するためにコード生成を利用します。モックの使い勝手はOCMockに似ています。OCMockはクラスやプロトコルのモックを作ることができますが、Structに対してはできません。このライブラリを使うことで、Structについてもメソッドをスタブ化して、呼び出した後に振る舞いを検証することができるようになります。

そうだとすると、 Gary Bernhardt氏が“Boundaries”の講演で述べている、あるいはAndy Matuschak氏の講演、“Swiftの複雑性をコントロールする”で述べられている新しい世界に私たちは向かっているのではないでしょうか 。ファンクショナルコアやインペラティブシェルといったものでコードベースが構築されていたら、モックを作る必要がありません。ファンクショナルコアは値型のみでできています。インペラティブシェルはとても薄いのでインテグレーションテストが通ればそれで確かめられます。

Swiftの世界を探求していて、このようなアーキテクチャはプロダクションにはまだ早いと思っていました。ですので、私は先日のAyakaさんの講演を聞いてとてもワクワクしています。このようなアーキテクチャがプロダクション環境のiOSアプリで使われていると初めて聞いたのですから。

けれども、大半の環境はここまでには至っていないと思います。そのため、アプリのアーキテクチャから書き換えない限りは、モックを使って良いテストを書いていきましょう。

まとめ (26:00)

コードベースを改善したければ、テストを書きましょう。モックを適切に使うことで優れたテストを書くことができます。この講演を聞いて、シンプルなモックが作れるようになったと思います。モックを書き、上手に使いましょう。

💪

About the content

2017年3月のtry! Swift Tokyoの講演です。映像はRealmによって撮影・録音され、主催者の許可を得て公開しています。

Veronica Ray

Veronica Ray is a software engineer at LinkedIn on the Video team. Once, she rode her bike between two moose. She blogs on Medium and is on Twitter as @nerdonica

4 design patterns for a RESTless mobile integration »

close