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


# はじめに

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

https://github.com/Matejkob/swift-spyable

# 概要

## swift-spyable に関して

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

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

https://github.com/krzysztofzablocki/Sourcery

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

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


### Sourcery からの移植

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

https://forums.swift.org/t/introducing-spyable-a-swift-macro-for-automatic-spies-generation/65572

> 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/25a33307-e56e-9647-4348-e4a5a8c254f3.png)

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

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

![Screenshot 2024-01-19 at 19.32.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/ea5e1089-f26c-23c8-b453-c73c63e934cb.png)

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

## swift-spyable の Tips

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

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

```.swift
@Spyable(behindPreprocessorFlag: "DEBUG")
```

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

```.swift
#if DEBUG

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

#endif
```

実際の例はこちらです。

![Screenshot 2024-01-19 at 19.41.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/cdf959f2-fc7b-ac46-ff10-9d7444289d21.png)

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

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


![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3137353234322f36313434366439312d353537362d373836352d353739372d6433303836616666306337332e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/3888f70f-0833-7757-1f86-1fab1808d44a.png)


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

https://github.com/Matejkob/swift-spyable/issues/79

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

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

### - CI 環境で使う

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

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

https://zenn.dev/miharun/articles/faff7a8aea9486

<br>

`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` があり、こちらにセットすることで機能します。

https://github.com/bitrise-steplib/steps-xcode-archive

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


```.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` として上がっており

https://github.com/Matejkob/swift-spyable/issues/72

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

<br>

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

https://github.com/Matejkob/swift-spyable/pull/73

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

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

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

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

https://github.com/Matejkob/swift-spyable/issues/19

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

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

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

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

```.swift
var trackErrorScreenCallsCount = 0 // <- １つ目

/* 略 */

func trackError(_ error: Error, screen: EventScreenType) {
    trackErrorScreenCallsCount += 1
    /* 略 */
}
```

```.swift
var trackErrorScreenCallsCount = 0 // <- ２つ目

/* 略 */

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

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

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

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

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

https://github.com/Matejkob/swift-spyable/issues/52

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

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

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

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


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

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


# 終わりに

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

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

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