1
4

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 3 years have passed since last update.

iOS #2Advent Calendar 2019

Day 17

【スナップショットテスト】mockを用いて動的な画面を生成する⭐️

Last updated at Posted at 2019-12-16

概要

UnitTestを用いたスナップショットテストは色々なメリットがありますʕ•ᴥ•ʔノ🌟✨
例えば、

⭐️アプリ内のあらゆる画面のスクリーンショットを各種デバイスで自動で行う
   →デザイナーにスクショを共有する際の用意工数の削減
   →各種サイズの端末を用意しなくて済む
⭐️レイアウト差分を検出できる
   →意図しない変更を検知できる

などなど💏

その際に各画面の表示ロジックを実行することになりますが動的な画面を作る際が少々厄介だったりします😫
というのも、表示する際に通信を行いその結果で描画を行う画面の場合、
何にも考慮せずにそのまま画面を呼び出すと、、、、

理想👍

スクリーンショット 2019-12-17 0.45.01.png

現実🌀

スクリーンショット 2019-12-17 0.25.24.png

というように、必要な通信が行われず、
空っぽのViewControllerを取得する結果となってしまいます😱

ということで今回はスナップショットテストを導入する際などに使える、
動的な画面をMockを使用して生成できるようにする方法を紹介しようと思います\\\٩( 'ω' )و ////

※スナップショット全体の説明については以下のスライドが参考になるので合わせて!
今回はスライドでは細かく触れられていないMock生成部分の具体例について書こうと思います。
🌟スナップショットテスト実戦投入 / Practical Snapshot Testing

前準備

- 前提

  • 導入したプロジェクトの設計はMVP+CleanArchitectureとしてます
    • この設計である必要はありませんが、DIという概念が必要となってきますʕ•ᴥ•ʔ以下の「設計」で後述します

- UseCaseMockの作成

  • Cuckooのインストール
  • BuildPhraseに以下を記載し、ビルド時にUseCaseのMockを作成するshellを実行するようにする
スクリーンショット 2019-12-17 3.17.21.png
プロジェクト/shell/generate_usecase_mocks.sh
set -eu

USE_CASE_OUTPUT_FILE = "$PROJECT_DIR/Mocks/GeneratedUseCaseMocks.swift"
USE_CASE_PROTOCOL_INPUT_DIR = "${PROJECT_DIR}/Domain/UseCase/Protocol"

"${PODS_ROOT}/Cuckoo/run" generate --testable "$PROJECT_NAME" --output "${USE_CASE_OUTPUT_FILE}" \
$USE_CASE_PROTOCOL_INPUT_DIR/*.swift

と、USE_CASE_PROTOCOL_INPUT_DIR階層内のメソッドが全てMock化されます\\\٩( 'ω' )و ////
USE_CASE_PROTOCOL_INPUT_DIR内には、PresenterからUseCaseに処理を委譲するためのProtocolファイル群が格納されています

protocol XxxUseCase: NSObjectProtocol {
    func getData()
...
}

↓ 自動生成される
image.png

画面の実装

ViewController
protocol XxxViewable: NSObjectProtocol {
    func xxx()
}

class XxxViewControllerFactory {
    static func create() -> XxxViewController {

        let vc = UIViewController.getViewControllerByStoryboard(storyboardName: "xxx",
                                                                            storyboardId: "xxx") as! XxxViewController
        viewController.presenter = PresenterBuilder().createXxxPresenter(view: viewController)
        return viewController
    }
}

class XxxViewController {
    fileprivate var presenter: XxxPresentable?

    override func viewDidLoad() {
        super.viewDidLoad()
        self.presenter?.didLoad()
    }
}

extension XxxViewController: XxxViewable {
    func xxx() {
        print("")
    }
}
Presenter
protocol XxxPresentable: NSObjectProtocol {
    func didLoad()
}

class XxxPresenter: XxxPresentable {
    private weak var view: XxxViewable?
    private let xxxUseCase: XxxUseCase

    init(view: XxxViewable,
         xxxUseCase: XxxUseCase) {
        self.view = view
        self. xxxUseCase = xxxUseCase
    }

    func didLoad() {
        self.view?.xxx()
    }

上記のコードのポイントは、
VCが持つPresenterにPresenterBuilder().createFixedPhrasePresenter(view: viewController)を代入している点で、
これがいわゆる「依存性注入(DI)」で、今回のテーマを実現するために必要となってきます⭐️

というのも、
UnitTestでは実際の通信が行われないため、

普通にビルドする場合:実際に通信したデータを持つUseCaseを使用する
UnitTestでビルドする場合:Mockデータを持つUseCaseを使用する

必要があり、
そのためそれぞれのフェーズで向き先を変えて、presenterにinitされるuseCaseを切り替える必要があるからです🌟

(呼び出しクラスの切り替えについてはスナップショットテスト実行時に、
BuildPhrasesでsedを使用して文字列差し替えをする形などで実現すれば良いかなと思います)

sed -E 'PresenterBuilder/PresenterBuilderForAutoLayoutTests/'

DIの実装

普通にビルドする場合のDI

→実際に通信したデータを持つUseCaseを使用する

PresenterBuilder
class PresenterBuilder {
    public func createXxxPresenter(view: XxxViewable) -> XxxPresentable {
        return XxxPresenter(view: view,
                            xxxUseCase: self.makeXxxUseCase())
    }

    private func makeXxxUseCase() -> XxxUseCase {
        return XxxUseCaseImpl(xxxRepository: self.xxxRepository()())
    }
}

UnitTestでビルドする場合のDI

→Mockデータを持つUseCaseを使用する
前提で作成したusecaseのモックを使用します。

PresenterBuilderForAutoLayoutTests

import Cuckoo
import RxSwift

class PresenterBuilderForAutoLayoutTests {
    public func createXxxPresenter(view: XxxViewable) -> XxxPresentable {
        return XxxPresenter(view: view,
                            xxxUseCase: self.makeXxxUseCase())
    }

    private func makeXxxaUseCase() -> XxxUseCase {
        let mock = MockXxxUseCase()
        stub(mock) { stub in
            when(stub.getData()).then {
                return Single.create { subscriber in
                    subscriber(.error(NSError(domain: "", code: NSURLErrorUnknown, userInfo: nil)))
                    return Disposables.create()
                }
            }
        }
        return mock
    }
}

テストの実行

最後にUnitTestを記述します。
メインキューで囲っているのは、FIFOでRxの処理(=VCの表示ロジック)が終わった後にVCをprintしたいためですʕ•ᴥ•ʔ

func testViewController() {
        let vc = XxxViewControllerFactory.create()
        DispatchQueue.main.async {
            print(vc)
        }
    }
}

Mock化するくだりの処理を行っていれば、以下の処理で、
理想👍が実現できているかと思います🎉🎉🎉🎉
スクリーンショット 2019-12-17 0.45.01.png

最後に

スナップショットテストと同時に実行できるため、
tarunonさんのAutoLayoutWarning検知のUnitTestを合わせて導入すると良いのかなと思ってます👍👍👍👍👍👍
https://github.com/tarunon/XCTAssertAutolayout

1
4
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
1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?