あるマクロ実装をマージしたところ、ローカルではすべてのテストに合格したにもかかわらず、クラウド Mac での次のビルドではメンバーの生成順序が変わり、診断位置も 1 行ずれていました。最終的な成果物は引き続きコンパイルできましたが、コードレビューで確認された API は同一ではありませんでした。Swift マクロはコンパイル時に実行され、その結果はマクロ実装、SwiftSyntax の依存関係、現在のツールチェーンのすべてに左右されます。そのため、通常の関数と同じ方法でテストするだけでは不十分です。
固定すべき結果を先に定義する
回帰テストでは 3 つのレイヤーを対象にします。第 1 レイヤーでは、マクロ展開後のソースコード全体を比較し、メンバー、アクセスレベル、属性、フォーマットの変化を検出します。第 2 レイヤーでは、エラー、警告、修正候補を検証し、特に診断が示す token を確認します。第 3 レイヤーで初めてコンパイルと実行をテストし、生成されたコードを実際に呼び出せることを確認します。
| レイヤー | 確認対象 | 典型的な失敗 |
|---|---|---|
| 展開レイヤー | 生成されたソースコードと宣言順序 | 属性の欠落、アクセスレベルの変化 |
| 診断レイヤー | メッセージ、位置、修正候補 | 誤ったノードにエラーが付く |
| 振る舞いレイヤー | コンパイル結果と実行結果 | 生成された実装はコンパイルできるが、動作が誤っている |
「テストがコンパイルできる」ことを「マクロの出力が変化していない」ことと同一視しないでください。多くの場合、マクロが生成するソースコードの形状そのものが公開契約です。
まず最小限の入力を選んでベースラインを作成し、その後に空の宣言、ジェネリクス、ネストした型、同名の既存メンバー、不正な引数を追加します。各テストケースでは 1 つのルールだけを検証し、失敗時に実装の回帰なのかツールチェーンの変化なのかを迅速に判断できるようにします。
展開テストで生成ソースコードを固定する
マクロパッケージのテストターゲットでは SwiftSyntaxMacrosTestSupport を導入し、assertMacroExpansion に入力と期待するソースコードを渡せます。次の例では、TraceIDMacro が構造体に文字列プロパティを追加するものとします。
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest
@testable import TraceIDMacros
private let macros: [String: Macro.Type] = [
"TraceID": TraceIDMacro.self
]
final class TraceIDMacroTests: XCTestCase {
func testAddsTraceID() {
assertMacroExpansion(
"""
@TraceID
struct Job {}
""",
expandedSource:
"""
struct Job {
let traceID: String
}
""",
macros: macros
)
}
}
期待値には完全なテキストを記述し、traceID が含まれているかだけを検索してはいけません。部分文字列によるアサーションでは、メンバーの重複、不正なインデント、意図せず追加されたインターフェースを見落とします。マクロが引数を受け取る場合は、デフォルト値、明示的な値、不正な式について、それぞれ個別のテストケースを作成します。
エラーパスを個別に分離する
不正な入力を、正常に展開されるケースと同じテストに混在させないでください。診断テキスト、行と列の位置、修正候補を個別に検証し、テスト名には発生条件を明記します。これにより、1 つのエラーメッセージを変更しただけで、正常系のスナップショット全体をチームが承認し直す必要がなくなります。
ツールチェーンのフィンガープリントを記録する
BookaMac のクラウド Mac では同じパイプラインを長期間実行できますが、ノードが安定していてもツールチェーンが永遠に変わらないとは限りません。テストを実行するたびに、選択されている Xcode のパス、Swift のバージョン、ホストアーキテクチャ、依存関係のロックファイルをまとめて保存します。
set -euo pipefail
export LANG=C
export LC_ALL=C
mkdir -p artifacts
xcode-select -p | tee artifacts/xcode-path.txt
xcrun swift --version | tee artifacts/swift-version.txt
uname -m | tee artifacts/host-arch.txt
cp Package.resolved artifacts/Package.resolved
xcrun swift test 2>&1 | tee artifacts/swift-test.log
LANG と LC_ALL を固定すると、実行アカウントの言語環境によって診断テキストが変化しにくくなります。xcrun を使用することで、shell のパスに偶然存在する別の swift ではなく、現在選択されている Xcode ツールチェーンでテストを実行できます。リポジトリに Package.resolved がない場合、空ファイルを作成してロック済みの結果に見せかけるのではなく、スクリプト側でコピーを明示的にスキップする必要があります。
並行実行と共有状態を分離する
マクロ実装が環境変数、カレントディレクトリ、時刻、一時ファイルを参照すると、テスト結果は並行実行時に不安定になりやすくなります。より堅牢な原則は、マクロが構文木と明示的な引数だけに基づいて結果を生成するようにすることです。外部データが本当に必要な場合は、読み取り処理を独立した型へ切り出し、テストでは固定値を注入します。
まず最小限のテストセットを直列モードで成功させ、その後に並列テストを有効にします。並列化した後にだけ失敗する場合は、リトライ回数を増やすのではなく、固定されたファイル名、共有キャッシュディレクトリ、グローバルな可変変数、テストのクリーンアップ順序を優先的に確認します。リトライは競合状態を断続的な成功として覆い隠してしまいます。
マクロテストで実際のプロジェクトのソースコードを変更してはいけません。一時的な入力はテストケースごとに独立したディレクトリへ書き込み、完了後に削除します。失敗時には、機密情報を除去した入力、展開結果、ログだけを残します。これにより再現性を確保しながら、前回の実行で残ったデータが次回の実行に影響することも防げます。
ツールチェーンを安全にアップグレードする
Xcode または SwiftSyntax をアップグレードする前に、まず旧ツールチェーンで全テストを実行してベースラインを保存し、その後ツールチェーンを切り替えて同じコマンドを実行します。差分は次の順序で確認します。
- 実際に選択されている Xcode のパスと Swift のバージョンを確認する。
Package.resolvedが想定どおりに変化しているか確認する。- 空白やインデントなどのフォーマット変更と、メンバー、型、アクセスレベルなどの意味的な変更を区別する。
- 振る舞いテストを再実行し、生成されたインターフェースを呼び出せることを確認する。
- レビューが完了してから、展開結果の期待値を更新する。
すべてのテストケースで同様のフォーマット差分が発生した場合は、通常、まず出力規則やツールチェーンの変化を調査します。境界条件を扱う 1 つのテストケースだけが変化した場合は、マクロ実装自体に問題がある可能性が高くなります。スナップショットを一括置換で直接更新しないでください。本当の API 回帰が大量のテキスト変更に紛れてしまいます。
最終的には、機械可読なテスト結果、展開失敗時の実際の結果と期待値の差分、ツールチェーンのフィンガープリントという 3 種類の成果物を保存します。次回クラウド Mac のビルドが失敗しても、調査を「環境が違う可能性がある」という推測から始める必要はありません。どのレイヤーの契約が、どのバージョンから変化したのかを明確に特定できます。
よくある質問
最終ビルドが成功すればマクロ展開テストは不要ですか?
必要です。ビルド成功だけでは生成 API の形や診断位置の変化を検出できません。展開ソースとエラー、警告、修正候補を個別に検証します。
Xcode 更新後に多数の期待値が変わった場合はどうしますか?
Xcode の選択先、Swift のバージョン、依存関係のロックファイルを比較し、整形差分と意味的変更を分離してから期待値を更新します。
検証済みのワークフローを常時稼働するクラウドMacへ
用途に合わせてBookaMac M4またはBookaMac M4 Proを選び、注文時にリージョン、契約期間、ストレージの追加オプションをご確認ください。