swift-snapshot-testingを使って、アプリを起動せずにSwiftUIのスナップショットテストをやってみた

swift-snapshot-testingを使って、アプリを起動せずにSwiftUIのスナップショットテストをやってみた

2026.02.12

これまでソースコードを修正するたびに、「他の画面のレイアウトを崩していないだろうか」と不安に思っていた。UIのテストを導入すれば解決できるとわかっていても、Xcode標準のXCUITestはアプリの起動やシミュレータ上での操作が必要であり、セットアップや継続的なメンテナンスの負担が大きい。そのため、UIテストの導入はずっと見送ってきた。

最近ではAIにスクリーンショットを見せてUIの改善サイクルを回すような開発手法も注目されており、スナップショットの活用に改めて関心が高まっている。そうした中で見つけたのが「swift-snapshot-testing」だ。このライブラリを使うことで、アプリを起動することなく、ユニットテストの延長としてSwiftUIのViewを画像に変換し、以前のスナップショットと自動比較できる。

本記事では、swift-snapshot-testingの導入からスナップショットが生成されるまでの基本的な流れを紹介する。

スナップショットテストとは

スナップショットテストは、ある時点のUIの見た目を画像として保存(記録)しておき、以降のテスト実行時にレンダリング結果と保存済みの画像を比較するテスト手法だ。

通常のユニットテストが「値が期待通りか」を検証するのに対し、スナップショットテストは「見た目が前回と変わっていないか」を検証する。

XCUITestとの大きな違いは以下の点である。

  • アプリの起動が不要 — ユニットテストターゲットで実行でき、UIHostingController 経由でViewを直接レンダリングするため、シミュレータ上でアプリを操作する必要がない
  • 条件の整備が不要 — テストコード上で状態を直接渡せるため、特定の画面状態を再現するためにアプリ内で画面遷移する手間がかからない
  • 高速 — アプリの起動・画面遷移を待つ必要がないため、1テストあたりの実行が非常に速い

検証環境

  • MacBook Pro (16インチ, 2023), Apple M2 Pro
  • macOS 16.7.3
  • Xcode 26.2
  • iPhone 17 Pro シミュレータ / iOS 26.1
  • swift-snapshot-testing 1.18.9

セットアップ

1. Swift Package Managerでの追加

XcodeでPackage Dependenciesを開き、[+]ボタンをクリックする。

スクリーンショット 2026-02-12 10.48.07

検索欄に以下のURLを入力する。その後、swift-snapshot-testing を選択して、[Add Package]ボタンをクリックする。

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

スクリーンショット 2026-02-12 10.48.20

下図のパッケージを追加し、[Add Package]ボタンをクリックする。パッケージを追加する際、SnapshotTesting モジュールをテストターゲットに追加すること。メインのアプリターゲットには追加しないよう注意が必要だ。

スクリーンショット 2026-02-12 10.48.50

Package.swift を使用している場合は以下のように記述する。

dependencies: [
  .package(
    url: "https://github.com/pointfreeco/swift-snapshot-testing",
    from: "1.18.9"
  ),
],
targets: [
  .target(name: "MyApp"),
  .testTarget(
    name: "MyAppTests",
    dependencies: [
      "MyApp",
      .product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
    ]
  )
]

2. テスト対象のViewを用意する

今回は例として、シンプルなプロフィールカードのViewを用意した。

import SwiftUI

struct ProfileCardView: View {
    let name: String
    let role: String

    var body: some View {
        VStack(spacing: 12) {
            Image(systemName: "person.circle.fill")
                .resizable()
                .frame(width: 80, height: 80)
                .foregroundStyle(.blue)

            Text(name)
                .font(.title2)
                .fontWeight(.bold)

            Text(role)
                .font(.subheadline)
                .foregroundStyle(.secondary)
        }
        .padding(24)
        .background(
            RoundedRectangle(cornerRadius: 16)
                .fill(.background)
                .shadow(radius: 4)
        )
    }
}

はじめてのスナップショットテストを書く

基本的なテストの記述

テストターゲットに新しいファイルを作成し、以下のように記述する。

import SnapshotTesting
import Testing
@testable import MyApp

@MainActor
struct ProfileCardViewTests {

    @Test
    func プロフィールカードの表示() {
        let view = ProfileCardView(name: "和田健司", role: "iOSエンジニア")

        assertSnapshot(
            of: view,
            as: .image(layout: .device(config: .iPhone13Pro))
        )
    }
}

ポイントをいくつか解説する。

  • import SnapshotTesting でライブラリをインポートする
  • SwiftUIのViewをレンダリングするために@MainActor が必要
  • assertSnapshot(of:as:) がコアのAPIである。第一引数にスナップショットを撮りたいViewや画面、第二引数にストラテジー(.image など)を渡す
  • SwiftUI Viewを渡すと、ライブラリが内部で UIHostingController にラップしてレンダリングしてくれる

ここで .iPhone13Pro を指定しているのは、swift-snapshot-testingが提供するプリセットのデバイスサイズを使用するためだ。「iPhone 13 Proの画面サイズでスナップショットを生成する」という指定であり、実行しているiOSシミュレータのデバイスと合わせる必要はない。

初回実行時に画像を記録する(テストは必ず失敗する)

テストを実行(Cmd + U)すると、初回は必ずテストが失敗する

これはバグではなく意図された動作だ。ディスク上に参照画像がまだ存在しないため、ライブラリが自動的にスナップショットを生成・保存した上で、以下のようなメッセージとともにテストを失敗させる。

Issue recorded: No reference was found on disk. Automatically recorded snapshot: …
open "file:///{{PATH}}/プロフィールカードの表示.1.png"
Re-run "プロフィールカードの表示" to assert against the newly-recorded snapshot.

スクリーンショット 2026-02-12 13.11.38

このタイミングで、テストファイルと同じ階層に __Snapshots__ ディレクトリが自動生成され、その中に参照画像が保存される。

MyAppTests/
├── ProfileCardViewTests.swift
└── __Snapshots__/
    └── ProfileCardViewTests/
        └── プロフィールカードの表示.1.png

スクリーンショット 2026-02-12 13.12.24

生成された画像は必ず目視で確認すること。 スナップショットテストは「前回から変わっていないこと」を検証するものなので、最初の記録が不具合のある状態であれば、以降ずっとその不具合を正として扱ってしまう。

また、下図の通り、シミュレータで実行したときとスナップショットテストで生成された画像の見た目は少し異なる。スナップショットはあくまでもテストで利用するにとどめ、最終的な動作確認はシミュレータまたは実機のiPhoneデバイスで実施すべきだ。

シミュレータ実行時 スナップショットテスト時
Simulator Screenshot - iPhone 17 Pro - 2026-02-12 at 13.19.03 プロフィールカードの表示.1

2回目の実行 — 比較テスト

参照画像の内容を確認したら、もう一度テストを実行する。今度は保存済みの参照画像と新しくレンダリングした画像をピクセル単位で比較し、一致すればテストがパスする。

もしViewに何らかの変更が加わっていた場合はテストが失敗し、Xcodeのテスト結果に参照画像・実行結果画像・差分画像のファイルパスが表示される。

トラブルシューティング

非決定的なデータに注意する

日付や乱数、ネットワーク経由の画像など、実行のたびに変わるデータがViewに含まれていると、毎回スナップショットが変わってテストが失敗する。テスト時はモックデータを使い、決定的な値を渡すようにすること。

また、アニメーションもスナップショットの不安定さの原因になる。テストのsetupで無効化しておくのが安全だ。

// Swift Testingの場合
init() {
    UIView.setAnimationsEnabled(false)
}

.onAppearの副作用に気をつける

SwiftUIの .onAppear はスナップショットのレンダリング中にも発火する。API呼び出しなどの副作用がある場合、テスト時にはそれらが実行されないよう、DIやモック化で対処する必要がある。

モーダル表示はキャプチャされない

.sheet().fullScreenCover() で表示されるモーダルは、スナップショットテストではキャプチャされない。モーダルの中身をテストしたい場合は、モーダル内で表示するViewを独立したコンポーネントとして切り出し、そのView単体でスナップショットテストを書くとよい。

__Snapshots__ディレクトリをGitにコミットする

__Snapshots__ ディレクトリはGitにコミットしておくこと。これがないとCI上で参照画像が見つからず、テストが常に失敗する。リポジトリサイズが気になる場合は、Git LFSの導入を検討してもよいだろう。

Liquid Glassと相性が悪い

iOS 26からLiquid Glass(リキッドグラス)デザインが導入された。撮影したスナップショットを確認すると、下図のようにLiquid Glassエフェクトが適用されていないことがわかる。

現時点では、この問題に対する明確な回避策は見つかっていない。swift-snapshot-testingは UIHostingController 経由でViewをレンダリングしているため、Liquid Glassのエフェクトが完全には再現されないものと推測される。スナップショットテストではLiquid Glassのエフェクトを除いた状態でのUI検証になるという制約を理解した上で運用する必要がある。

まとめ

swift-snapshot-testingを使えば、アプリを起動せずにSwiftUIのViewを画像としてキャプチャし、意図しないUI変更を検出できる。XCUITestと比べてセットアップが手軽で、ユニットテストの延長として気軽に導入できるのが大きな魅力だ。また、冒頭で触れたように、生成されたスナップショットをAIに確認してもらい、デザインをブラッシュアップするといった活用も期待できる。

一方で、非決定的なデータや .onAppear の副作用など、スナップショットテスト特有の注意点もある。まずは1つのViewから試してみて、スナップショットテストの手軽さを実感してみてほしい。

SwiftUIのUIテスト導入に悩んでいる方の参考になれば幸いだ。

参考リンク

この記事をシェアする

FacebookHatena blogX

関連記事