概要
UnitTestを用いたスナップショットテストは色々なメリットがありますʕ•ᴥ•ʔノ🌟✨
例えば、
⭐️アプリ内のあらゆる画面のスクリーンショットを各種デバイスで自動で行う
→デザイナーにスクショを共有する際の用意工数の削減
→各種サイズの端末を用意しなくて済む
⭐️レイアウト差分を検出できる
→意図しない変更を検知できる
などなど💏
その際に各画面の表示ロジックを実行することになりますが動的な画面を作る際が少々厄介だったりします😫
というのも、表示する際に通信を行いその結果で描画を行う画面の場合、
何にも考慮せずにそのまま画面を呼び出すと、、、、
理想👍
現実🌀
というように、必要な通信が行われず、
空っぽのViewControllerを取得する結果となってしまいます😱
ということで今回はスナップショットテストを導入する際などに使える、
動的な画面をMockを使用して生成できるようにする方法を紹介しようと思います\\\٩( 'ω' )و ////
※スナップショット全体の説明については以下のスライドが参考になるので合わせて!
今回はスライドでは細かく触れられていないMock生成部分の具体例について書こうと思います。
🌟スナップショットテスト実戦投入 / Practical Snapshot Testing
前準備
- 前提
- UseCaseMockの作成
-
Cuckooのインストール
- →モックを自動生成してくれる&テストをサポートしてくれるフレームワーク
- →Cuckooを使ったiOSアプリのユニットテスト
- BuildPhraseに以下を記載し、ビルド時にUseCaseのMockを作成するshellを実行するようにする
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()
...
}
画面の実装
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("")
}
}
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を使用する
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のモックを使用します。
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化するくだりの処理を行っていれば、以下の処理で、
理想👍が実現できているかと思います🎉🎉🎉🎉
最後に
スナップショットテストと同時に実行できるため、
tarunonさんのAutoLayoutWarning検知のUnitTestを合わせて導入すると良いのかなと思ってます👍👍👍👍👍👍
https://github.com/tarunon/XCTAssertAutolayout