CI でモックやスタブの実装をせず API レスポンスを差し替える
はじめに
iOS アプリの UI テストや結合テストを CI で実行するとき、外部 API への依存がよく問題になります。テスト実行時に API がダウンしていたり、レスポンスが変わっていたりすると、テストが不安定になりがちです。
この記事では、HTTP/HTTPS プロキシ & モックツール「Mimicry」の CLI スタンドアロンモード (mimicry serve) を使い、Xcode Cloud 上で API モックを動かす方法を紹介します。
mimicry serve コマンド
Mimicry はもともと GUI (Tauri + React) ベースのデスクトップアプリですが、mimicry serve サブコマンドを使えば GUI なしでプロキシ & モックが完結します。
# JSON ファイルからモックルールを読み込んでプロキシを起動
mimicry serve --rules ./mock-rules.json --port 8080
# iOS シミュレーターのプロキシ設定も自動構成(ローカル開発向け)
mimicry serve --rules ./mock-rules.json --port 8080 --configure-simulator
HTTPS インターセプトにも対応していて、CA 証明書の自動生成や指定が可能です。
Xcode Cloud での利用
スクリプト構成
Xcode Cloud では ci_pre_xcodebuild.sh の中で Mimicry のダウンロード・インストールとプロキシ起動をまとめて行います。このスクリプトは各 xcodebuild アクションの直前に実行されるため、テスト開始時にプロキシが確実に動作しています。
ci_pre_xcodebuild.sh はビルドフェーズでもテストフェーズでも呼ばれるため、CI_XCODEBUILD_ACTION 環境変数でテスト実行時のみに絞り込む必要があります。
ci_pre_xcodebuild.sh
CI_XCODEBUILD_ACTION が test-without-building のときだけ Mimicry のインストールとプロキシ起動を行います。Xcode Cloud のテストフェーズではリポジトリのファイルが存在しないため、モックルールの JSON もスクリプト内で生成しています。
#!/bin/bash
set -euo pipefail
if [[ "${CI_XCODEBUILD_ACTION:-}" != "test-without-building" ]]; then
exit 0
fi
MIMICRY_VERSION="5.6.4"
MIMICRY_DIR="${CI_PRIMARY_REPOSITORY_PATH:-/Volumes/workspace/repository}/.mimicry"
MIMICRY_BIN="$MIMICRY_DIR/Mimicry.app/Contents/MacOS/Mimicry"
if [ ! -f "$MIMICRY_BIN" ]; then
mkdir -p "$MIMICRY_DIR"
curl -sL -o "$MIMICRY_DIR/Mimicry.dmg" \
"https://github.com/kusumotoa/mimicry-releases/releases/download/v${MIMICRY_VERSION}/Mimicry_${MIMICRY_VERSION}_universal.dmg"
hdiutil attach "$MIMICRY_DIR/Mimicry.dmg" -nobrowse -quiet
cp -R /Volumes/Mimicry/Mimicry.app "$MIMICRY_DIR/Mimicry.app"
hdiutil detach /Volumes/Mimicry -quiet
fi
# テストフェーズではリポジトリのファイルが存在しないためスクリプト内で生成
cat > "$MIMICRY_DIR/mock-rules.json" <<'RULES'
{
"rules": [
{
"id": "test-hello",
"urlPattern": "/hello",
"method": "GET",
"statusCode": 200,
"responseBody": "{\"message\":\"Hello from Mimicry!\"}",
"headers": { "Content-Type": "application/json" },
"enabled": true,
"domains": ["kusumoto-test-no-domain"]
}
],
"allowedDomains": ["kusumoto-test-no-domain"],
"noCaching": true
}
RULES
nohup "$MIMICRY_BIN" serve \
--rules "$MIMICRY_DIR/mock-rules.json" \
--port 8080 > "$MIMICRY_DIR/mimicry.log" 2>&1 &
sleep 3
実際の Xcode Cloud のログを見ると、ビルドフェーズでは Skipping (action: build-for-testing) と表示されてスキップされます。

テストフェーズでは Mimicry のダウンロードからプロキシ起動、モックレスポンスの検証まで一連の処理が実行されます。

APIClient のセッション生成
アプリの APIClient で URLSession を生成する箇所に #if DEBUG でプロキシ設定を追加します。リリースビルドでは通常の通信になるので、プロダクションへの影響はありません。
Mimicry は HTTPS 通信をインターセプトする際に自己署名の CA 証明書を使うため、URLSessionDelegate で証明書検証をスキップする必要があります。#if DEBUG で囲むのでリリースビルドには影響しません。
struct APIClient {
static func makeSession() -> URLSession {
let config = URLSessionConfiguration.default
#if DEBUG
config.connectionProxyDictionary = [
"HTTPSEnable": true,
"HTTPSProxy": "localhost",
"HTTPSPort": 8080,
]
return URLSession(configuration: config, delegate: ProxyTrustDelegate(), delegateQueue: nil)
#else
return URLSession(configuration: config)
#endif
}
}
#if DEBUG
private final class ProxyTrustDelegate: NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge) async
-> (URLSession.AuthChallengeDisposition, URLCredential?) {
(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
}
}
#endif
テストコード
import Testing
import Foundation
struct MimicryProxyTests {
@Test("Mimicry プロキシ経由でモックレスポンスが返る")
func mockResponseThroughProxy() async throws {
let session = APIClient.makeSession()
let url = URL(string: "https://kusumoto-test-no-domain/hello")!
let (data, response) = try await session.data(for: URLRequest(url: url))
let httpResponse = try #require(response as? HTTPURLResponse)
#expect(httpResponse.statusCode == 200)
struct HelloResponse: Decodable {
let message: String
}
let body = try JSONDecoder().decode(HelloResponse.self, from: data)
#expect(body.message == "Hello from Mimicry!")
}
}
モックルールの JSON 形式
mimicry serve は 2 種類の JSON 形式に対応しています。
シンプル形式はルール配列だけを記述します。
[
{
"id": "rule-1",
"urlPattern": "/api/users",
"method": "GET",
"statusCode": 200,
"responseBody": "{\"users\":[]}",
"headers": { "Content-Type": "application/json" },
"enabled": true,
"domains": ["api.example.com"]
}
]
フル形式はドメインフィルタやキャッシュ設定を含められます。
{
"rules": [...],
"allowedDomains": ["api.example.com"],
"ignoredDomains": ["*.apple.com"],
"noCaching": true
}
ハマったポイント
Xcode Cloud で動かすまでに遭遇した問題を紹介します。
1. フェーズ間でファイルもプロセスも保持されない
最初は ci_post_clone.sh で Mimicry をインストールしてプロキシを起動していましたが、テストフェーズでは /tmp のファイルもバックグラウンドプロセスも消えていました。Xcode Cloud はフェーズごとに環境がリセットされるようです。
インストール先をワークスペース内 ($CI_PRIMARY_REPOSITORY_PATH/.mimicry/) にし、プロキシの起動を ci_pre_xcodebuild.sh に移動することで解決しました。CI_XCODEBUILD_ACTION でテスト実行時のみに限定しています。
if [[ "${CI_XCODEBUILD_ACTION:-}" != "test-without-building" ]]; then
exit 0
fi
2. シミュレーターにプロキシ設定が反映されない
プロキシは起動しているのに、テストの HTTP リクエストがプロキシを経由しない問題に当たりました。
NetworkProxy.plist の書き込みや networksetup によるシステムプロキシ設定、launchctl setenv http_proxy による環境変数設定など、思いつく方法を一通り試しましたが、いずれも効きません。
CI 環境の診断ログを見てみると、Xcode Cloud は QEMU ベースの仮想マシンで動作しており、Wi-Fi サービスが存在しないことがわかりました。
[Serve] No network services found
--- Wi-Fi HTTP proxy ---
** Error: Unable to find item in network database.
| 方式 | 結果 |
|---|---|
| NetworkProxy.plist 書き込み | 効かない |
| networksetup | 効かない(サービスが存在しない) |
| launchctl setenv http_proxy | 効かない |
| connectionProxyDictionary | 動作する |
結局、テストコードで URLSessionConfiguration.connectionProxyDictionary を設定するのが唯一の方法でした。
3. HTTPS の CA 証明書が信頼されない
プロキシは経由するようになったものの、HTTPS 通信で証明書エラーが発生しました。Mimicry は HTTPS インターセプト時に自己署名の CA 証明書を使うため、シミュレーターにその CA を信頼させる必要があります。
simctl keychain add-root-cert はシミュレーターが起動していないと使えません。調べてみると、シミュレーターの証明書ストアは TrustStore.sqlite3 というファイルに保存されているようだったので、直接書き込みも試しました。しかし、シミュレーターが起動した時点でストアの内容がメモリにキャッシュされるらしく、後から書き換えても反映されませんでした。
最終的に URLSessionDelegate で #if DEBUG 時のみ証明書検証をスキップする方式で解決しました。リリースビルドではコンパイルされないので、本番への影響はありません。具体的なコードは前述の APIClient セクションを参照してください。
動作確認
すべての問題を解決した後、Xcode Cloud 上でテストが通りました。

Swift Testing のテスト結果で、iPhone 17 Pro Max シミュレーター上でモックレスポンスの検証が 0.1 秒で完了しています。
まとめ
今まで CI でのテストといえば、テストコード上でモックやスタブを用意するものだと思っていましたが、プロキシで API レスポンスを丸ごと差し替えるやり方もありだなと思いました。プロダクションコードに手を入れなくて済むのが気に入っています。CI 環境での API モックに悩んでいる方は、検討してみてください。