はじめに
Swift 5.9 でマクロが使えるようになりました!
今回はそんなマクロを使ったテスト向けのライブラリ swift-spyable
についてです。
概要
swift-spyable に関して
マクロを使ってテスト用のモック(スパイ)を自動で生成してくれるライブラリです。
今までもモックを自動で生成するライブラリはいくつか存在しました。特に有名なのは Sourcery
でしょう。
Stencil
を使ってプロジェクトに直接コードをはき出すような形式で、モックに限らず多くライブラリでこの方法が採用されています。(ex SwiftGen
, Kitura
)
このようなライブラリでは、生成されたコードをプロジェクトで管理する必要がありました。マクロの場合、コンパイル時に自動で内部生成してくれるので、その必要がありません。
Sourcery からの移植
swift-spyable
は、Sourcery
の AutoMockable
をベースにしているため、基本的にはそのまま移行することができます。
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
で導入します。(それ以外は用意されていないように見える)
/* 略 */
dependencies: [
.package(url: "https://github.com/Matejkob/swift-spyable", from: "0.3.0")
]
/* 略 */
/* 略 */
.product(name: "Spyable", package: "swift-spyable"),
/* 略 */
(※ 2024 / 2 時点での最新バージョンは 0.3.0
)
2. Spyable の実装
使用したい箇所でライブラリをインポートして
import Spyable
使用したい箇所に、作成したマクロ名のアノテーション @Spyable
を追加します。
@Spyable
protocol XxxRepositoryProtocol {
var models: [Model] { get }
func fetch() -> Model
func fetchAsync() async -> [Model]
}
予測変換に出てこない場合は、一度 プロジェクトをビルド or 再起動 しましょう。
実装はこれで完了です!
3. コードを確認する
アニテーション上で右クリックをして出てくる選択肢の中に 「Expand Macro」 があるので、クリックするとコードが展開されます。
ショートカットキーに登録されていないので Xcode
の
「Settings」>「Key Bindings」
で設定することをお勧めします。
実装したコードのすぐ下に、コードは展開されます。
このままテストで使用できるので便利です!
swift-spyable の Tips
- 生成先のスキーム指定をする
Spyable
のアノテーションには、スキームを指定できるオプションがあります。
@Spyable(behindPreprocessorFlag: "DEBUG")
指定すると、#if
~ #endif
で囲まれたコードになります。
#if DEBUG
/* 生成されるコード */
#endif
実際の例はこちらです。
このように展開されるコードが開発環境にしか出してほしくない場合などに有用です!
注意として、マクロの順番を気をつけてください。例えば、MainActor
と並列でつけることがあると思いますが、順番が違うと先ほどの指定が無意味になってしまいます。
このように、順番が違うだけで展開されるコードが違うので(今回の場合は @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 にする
スクリプトを作成して設定します。
#!/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
があり、こちらにセットすることで機能します。
以下に設定した例を置いておきます。
## 略 ##
- 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つの関数を用意しました。
func trackError(_ error: Error, screen: EventScreenType)
func trackError(_ type: EventErrorType, screen: EventScreenType)
これらはマクロで、それぞれ以下のように生成されます。
var trackErrorScreenCallsCount = 0 // <- 1つ目
/* 略 */
func trackError(_ error: Error, screen: EventScreenType) {
trackErrorScreenCallsCount += 1
/* 略 */
}
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
がメジャーバージョンになることを待ちつつ、細々と使っていこうかなと笑