LoginSignup
17
4

[Swift] swift-spyable を使って Mock を自動生成する

Last updated at Posted at 2024-01-20

はじめに

Swift 5.9 でマクロが使えるようになりました!
今回はそんなマクロを使ったテスト向けのライブラリ swift-spyable についてです。

概要

swift-spyable に関して

マクロを使ってテスト用のモック(スパイ)を自動で生成してくれるライブラリです。

今までもモックを自動で生成するライブラリはいくつか存在しました。特に有名なのは Sourcery でしょう。

Stencil を使ってプロジェクトに直接コードをはき出すような形式で、モックに限らず多くライブラリでこの方法が採用されています。(ex SwiftGen, Kitura

このようなライブラリでは、生成されたコードをプロジェクトで管理する必要がありました。マクロの場合、コンパイル時に自動で内部生成してくれるので、その必要がありません。

Sourcery からの移植

swift-spyable は、SourceryAutoMockable をベースにしているため、基本的にはそのまま移行することができます。

In order to ensure a smooth and seamless transition from Sourcery to the Spyable macro, I have taken steps in the initial version of Spyable. It's designed to generate spies for protocols in the same way as the AutoMockable template. Thanks to that, most projects using Sourcery with the basic AutoMockable template can be switched in minutes.

ただし、まだメジャーバージョン(1.0)ではないので、すべての機能が移植されてはいないように見えます。

swift-spyable を使ってみる

セットアップは基本的に README の通りに進めるだけです。

1. ライブラリの追加

SPM で導入します。(それ以外は用意されていないように見える)

Package.swift
/* 略 */

dependencies: [
  .package(url: "https://github.com/Matejkob/swift-spyable", from: "0.3.0")
]

/* 略 */
Package.swift
/* 略 */

.product(name: "Spyable", package: "swift-spyable"),

/* 略 */

(※ 2024 / 2 時点での最新バージョンは 0.3.0)

2. Spyable の実装

使用したい箇所でライブラリをインポートして

.swift
import Spyable

使用したい箇所に、作成したマクロ名のアノテーション @Spyable を追加します。

.swift
@Spyable
protocol XxxRepositoryProtocol {
    var models: [Model] { get }
    func fetch() -> Model
    func fetchAsync() async -> [Model]
}

予測変換に出てこない場合は、一度 プロジェクトをビルド or 再起動 しましょう。

実装はこれで完了です!

3. コードを確認する

アニテーション上で右クリックをして出てくる選択肢の中に 「Expand Macro」 があるので、クリックするとコードが展開されます。

Screenshot 2024-01-19 at 19.33.39.png

ショートカットキーに登録されていないので Xcode
「Settings」>「Key Bindings」
で設定することをお勧めします。

実装したコードのすぐ下に、コードは展開されます。

Screenshot 2024-01-19 at 19.32.48.png

このままテストで使用できるので便利です!

swift-spyable の Tips

- 生成先のスキーム指定をする

Spyable のアノテーションには、スキームを指定できるオプションがあります。

.swift
@Spyable(behindPreprocessorFlag: "DEBUG")

指定すると、#if ~ #endif で囲まれたコードになります。

.swift
#if DEBUG

/* 生成されるコード */

#endif

実際の例はこちらです。

Screenshot 2024-01-19 at 19.41.35.png

このように展開されるコードが開発環境にしか出してほしくない場合などに有用です!

注意として、マクロの順番を気をつけてください。例えば、MainActor と並列でつけることがあると思いますが、順番が違うと先ほどの指定が無意味になってしまいます。

68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3137353234322f36313434366439312d353537362d373836352d353739372d6433303836616666306337332e706e67.png

このように、順番が違うだけで展開されるコードが違うので(今回の場合は @MainActor の中身がブラックボックスなので)、生成物はちゃんと確認しておきましょう。

ちなみに Issue として上がっており PR もあるので、上記はそのうち解決するかもです👀

【追記】こちらはバージョン 0.3.0 で解決した模様です。

- CI 環境で使う

ローカルの Xcode では、マクロの初回実行時に1回だけ出現するアラート(Trust & Enable)だけで、気づかない人もいるとおもいますが、CI 環境ではフィンガープリント検証のチェックをしておかないと、エラーが出て止まってしまいます。

以下の記事がとてもわかりやすいです。


swift-spyable の場合は、CI 上で以下のようなエラー文言が発生します。

Testing failed:
	Target 'SpyableMacro' must be enabled before it can be used.
	Testing cancelled because the build failed.
 
** TEST FAILED **

The following build commands failed:
	ComputeTargetDependencyGraph

これを以下の対応で解決します。

方法① IDESkipMacroFingerprintValidation を YES にする

スクリプトを作成して設定します。

run_skip_macro_validation.sh
#!/bin/sh
defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES

(Xcode Cloud なら ci_post_clone.sh に記載すればOK)

次に実行権限を付与します。

chmod +x run_skip_macro_validation.sh

これを必要に応じて、CI のワークフローから呼び出すと良いでしょう。

方法② xcodebuild コマンドのオプションで設定する

※ こちらは Xcode Cloud では動かないので注意 ※

xcodebuild を使っている場合は、-skipMacroValidation オプションをつけるだけです。

xcodebuild -skipMacroValidation xxx xxx ...

おまけ - Bitrise 上で設定する

Bitrise のステップにある xcode-archive のオプションに xcodebuild_options があり、こちらにセットすることで機能します。

以下に設定した例を置いておきます。

.yml
## 略 ##

    - xcode-archive@5.0:
        inputs:
        - export_method: app-store
        - scheme: Your_App
        - distribution_method: app-store
        - compile_bitcode: 'no'
        - configuration: Release
        - export_development_team: 12AB3C4D6F
        - xcodebuild_options: -skipMacroValidation

## 略 ##

swift-spyable の課題

0.3.0 で起きている問題を記載します。
記事の執筆時よりアップデートされている可能性があるので、常に情報を確認しましょう!

- モジュールをまたぐ場合は使えない

最近はマルチモジュールで開発しているところも増えたと思いますが、現状 swift-spyable はモジュールをまたぐと参照できません。(バージョン 0.3.0時点)

ただし、これは ISSUE として上がっており

内容としては、生成されるコードが Public になっていないからのようです。


既に対応した PR も作成されており

絶賛レビュー中なので、そのうち対応がなされると思います。

どうしても先に使用したい場合は、実装元のブランチをフォークして自分のブランチとして参照しましょう。

- 同じような関数で引数を省略した場合のビルドエラー

似たような問題が Issue としてはすでに上がっています。

spyable で生成されるものは、関数名+引数名 の組み合わせにすることで、ユニークなものになるように異なっています。

しかし、適応されないものが、引数を省略した場合です。
実例として、2つの関数を用意しました。

.swift
func trackError(_ error: Error, screen: EventScreenType)
func trackError(_ type: EventErrorType, screen: EventScreenType)

これらはマクロで、それぞれ以下のように生成されます。

.swift
var trackErrorScreenCallsCount = 0 // <- 1つ目

/* 略 */

func trackError(_ error: Error, screen: EventScreenType) {
    trackErrorScreenCallsCount += 1
    /* 略 */
}
.swift
var trackErrorScreenCallsCount = 0 // <- 2つ目

/* 略 */

func trackError(_ type: EventErrorType, screen: EventScreenType) {
    trackErrorScreenCallsCount += 1
    /* 略 */
}

生成されている名前が重複しています。そのためビルドエラーが起きてしまいます。このように関数の引数を _ で省略をしている場合は、生成される名前から無視される ようです。

現状は、引数の省略しないことでしか対応できません。

- ジェネリクスが対応してない

こちらも Isuue としてあがっています。

回避策もないので、自前のモックを用意するしかない...

生成してくれたコードをベースに、ジェネリクス部分(例えば、Tなど)に

  • 制約があればその型にする
  • 制約がなければ Any にする

とすることで一時的に使用するのが早いでしょう。

- オプションや機能が少ない

先ほども記載しましたが、まだマイナーバージョンであるため機能が出揃っていません。
気になる方は、Sourcery の機能を移植した PR をつくりましょう!

終わりに

旧来の Stencil で生成されたコードを管理しなくて済むので、swift-spyable のようなマクロを使ったライブラリは、とても使い勝手がとても良いです。(swift-spyable が非常に軽量なのも良い)

ただ、マクロはコンパイルが失敗した時にデバッグしづらいのも事実であり、その点は旧来の生成物ベースの方がやりやすかったかもしれません。
(結局 Stencil コードを見ることにはなるが)

swift-spyable がメジャーバージョンになることを待ちつつ、細々と使っていこうかなと笑

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