SwiftUIをSnapShotsTestで検証する

# はじめに

[iOS #2 Advent Calendar 2020 9日目の記事](https://qiita.com/advent-calendar/2020/ios-2)です。

こんにちは。PORT新卒エンジニアの[しゅんいち（shxun6934）](https://twitter.com/shxun6934)です。

個人でSwiftUIをこっそり勉強して居たのですが、ふと思いました。

**「SwiftUIのUIテストって、どー書いたらいいの？？？」**

気になったので、いろいろ調べたらいいブログ記事を見つけました。

[**Testing SwiftUI Views**](https://www.vadimbulavin.com/snapshot-testing-swiftui-views/)

この記事で学んだことを自分なりに要約して、共有したいと思います！


# SwiftUIのテストの書き方

SwiftUIのテストは、**今までのUITestのようには書けない**ようです。

SwiftUIは、Viewに対して**アクセスするプロバイダ**を提供していないので、手動でViewにアクセスし、テストを検証するのが難しいです。

WWDC2019でAppleは、SwiftUIのテストに対しては、**テストしたいViewのプレビューをそれぞれ作成**し、それを検証する必要があると言っていました。

https://developer.apple.com/videos/play/wwdc2019/233/


しかし、このテスト方法は、**実際のViewをテストしているわけではない**ので、信頼性に少しかけるところがあります。
また、拡張性があまりないのも懸念されています。**他言語対応やテーマ（ライトとダーク）に対して**、それぞれのテストがあまりできないのもあるそうです。

これらの改善点は、**SnapShotTest**を使用することで改善されます！


# SnapshotTestとは、、、

SnapShotTestは、すでに構成されているViewから**スナップショットを生成し**、検証するViewが正しい状態であるかどうかを**スナップショットとの差分**から判別するテスト方法です。

詳しくは、ZOZOテクさんのブログに書いてあります！
[SnapShotTestとは](https://techblog.zozo.com/entry/ios_snapshottest)


## 従来のUIテストとの違い

今までのUIユニットテストは、いくつかのコードを実行して、**入力が正常に出力を生成するかどうか**を検証します。

一方でスナップショットテストでは、テストの出力を**ベースラインスナップショット**と呼ばれる、以前のテスト実行された出力と比較して、**正常になっているか**を検証します。


SnapShotTestを使用したSwiftUIのテストは、

1. SwiftUIで構成されたViewを**さまざまな状態でレンダリング**しておく
2. 一方で、**正常なViewのスナップショット**を取得する
3. 以前に作成した**ベースラインスナップショットと比較**する
4. 差がなかったら成功、あったら失敗する

のフローになります。

失敗した場合は、以下の2つのことが考えられますね。

1. 変更が意図されており、ベースラインスナップショットを更新する必要がある。
2. 予期しない変更であり、コードを修正する必要がある。

このように、UIを別のUIと比較して検証するのが、SnapShotTestの特徴です。
なので、UIを検証するユニットテストにかなり向いています！


# 実際に書いてみよう

なんとなく、SnapShotTestのことがわかったので、簡単なものを実際に書いていきましょう！


## swift-snapshot-testing

今回は、SwiftでSnapShotTestができる[swift-snapshot-testing](https://github.com/pointfreeco/swift-snapshot-testing)を使用していきます。

せっかくのなので、Xcode11から追加された、**Swift Package Manager**を使用してみましょう！


### 導入

`File > Swift Packages > Add Package Dependency`を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179470/2dac0601-8bb2-3d77-fd8d-3444370f8f7b.png)
スクリーンショット 2020-12-09 21.03.23.png


そしたら、追加するパッケージの入力する画面が出るので、

`https://github.com/pointfreeco/swift-snapshot-testing.git`

を入力し、Versionを指定します。（最新版で大丈夫そうです。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179470/14e05fcf-1568-66c2-3ba6-bce3de2d5a13.png)
スクリーンショット 2020-12-09 21.20.16.png


gitによってライブラリがXcodeに読み込まれます。

最後のダイアログには注意が必要です。

ライブラリを使用するTargetを指定するのですが、公式のREADMEにも書いてあるように、
デフォルトだと、プロジェクトのメインアプリケーション/フレームワークターゲットに設定してしまうので
テストターゲットにしてあげましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179470/897b0be7-7e8f-67c0-d04c-6da803c3e117.png)


## コード

記事と同じように、`SendButton`を作成します。

```swift:SendButton.swift
struct SendButton: View {
    let onAction: () -> Void = {}
    
    var body: some View {
        Button(
            action: onAction,
            label: {
                HStack {
                    Image(systemName: "square.and.arrow.up")
                    Text("送る")
                }
        })
    }
}

# プレビュー
struct SendButton_Preview: PreviewProvider {
    static var previews: some View {
        SendButton()
            .previewLayout(PreviewLayout.sizeThatFits)
            .padding()
    }
}
```

テストコードは以下。

```swift:SendButtonUITest.swift
import XCTest
import SnapshotTesting
import SwiftUI
@testable import SnapshotTestingSwiftUI

private let referenceSize = CGSize(width: 150, height: 50)

class SendButtonTests: XCTestCase {

    func testDefaultAppearance() {
        assertSnapshot(
            matching: SendButton().referenceFrame(),
            as: .image(size: referenceSize)
        )
    }
}

private extension SwiftUI.View {
    func referenceFrame() -> some View {
        self.frame(width: referenceSize.width, height: referenceSize.height)
    }
}

```

ここで、プロジェクトに成功する画像をあらかじめ入れておいてください。
そうするとテストが通ると思います！


# まとめ

結構簡単にかけるなーという印象です。
SnapshotTestという概念が個人的に新しい概念だと思うので、書いていて楽しかったですし、検証していることが正常なViewの検証なので、わかりやすいなと思いました。

これをもう少し使いこなせるようになりたいですね！！

（今回使用しているライブラリがなかなか読み込まれなくて苦戦しました。。。）