テストシミュレータで漏れた WKWebView ナビゲーションをキャンセル

hotwired/hotwire-native-ios

テスト実行時に実際のネットワークアクセスが発生し、共有 WKProcessPool がブロックされて CI が数分間停止していた問題を、ナビゲーションを即時 cancel することで解消し、テスト全体の実行時間を大幅に短縮しました。

背景

WebViewNavigationSimulatordecidePolicyFor が常に decisionHandler(.allow) を呼び出していたため、WKWebView が外部 URL(例: https://example.com)へ実際に遷移し、共有 WKProcessPool がロックされてテストが 12 分以上停滞していました。この挙動は CI での全テストスイート(208 件)で合計約 1018 秒の実行時間を引き起こしていました。

この問題は外部ネットワークアクセスが不要なテストシナリオでも発生しており、テストコード側でナビゲーションアクションのみ取得すれば十分という設計趣旨と乖離していました。実際に CI が長時間ブロックされていた事実は、PR のベンチマーク表でも「Before」側の数秒から数百秒へと跳ね上がっている点で裏付けられます。

技術的な変更

WebViewNavigationSimulator.decidePolicyFor 内で、decisionHandler に対する呼び出しロジックを分岐させ、キャプチャしたナビゲーションアクションが取得できた時点で decisionHandler(.cancel) を実行するようにしました。これにより実際のページ遷移は発生せず、WKProcessPool のロックも回避されます。

@@
-        decisionHandler(.allow)
+        // Capture the navigation action without performing the real navigation.
+        // Canceling prevents external URL loads that would block the shared WKProcessPool.
+        decisionHandler(.cancel)
         continuation?.resume(returning: navigationAction)
         continuation = nil

同時に、intermediate なナビゲーション(例: loadHTMLString による初期ロード)だけは decisionHandler(.allow) で許可し、ページが正しくロードされるように条件分岐を追加しました。また、テスト終了時に残っているロードを確実に停止させるため、stopLoading() メソッドを実装し、tearDown で呼び出すように変更しました。

@@
     func stopLoading() {
-        webView.stopLoading()
-        webView.navigationDelegate = nil
+        // Ensure no pending navigation remains after a test completes.
+        webView.stopLoading()
+        webView.navigationDelegate = nil
     }
@@
-        webNavigationSimulator = nil
+        webNavigationSimulator.stopLoading()
+        webNavigationSimulator = nil
@@
-        webNavigationSimulator = nil
+        webNavigationSimulator?.stopLoading()
+        webNavigationSimulator = nil

設計判断

実際の外部ナビゲーションを許可する代わりに即座に cancel する方針は、テストが外部環境に依存しないことを保証しつつ、WKProcessPool の競合を防止する最小限の変更と言えます。ナビゲーションをキャンセルしても WKNavigationAction オブジェクトは取得できるため、テストロジック自体は変更不要です。

stopLoading() の導入は、テスト終了後に残っているロードやデリゲート参照を明示的に解放する安全策です。これによりメモリリークや予期しないコールバックが起こるリスクを低減し、テストフレームワークのクリーンアップ手順を一元化しました。

この設計は、API の互換性 を保ちつつ、テスト実行速度を劇的に改善するという二重の目的を達成しています。既存のテストコードは改変不要で、追加したメソッドは内部使用に留められています。

まとめ

WebViewNavigationSimulator が外部 URL への実際の遷移を防ぎ、テストスイート全体の実行時間を約 95% 短縮しました。decisionHandler(.cancel)stopLoading() の組み合わせにより、共有 WKProcessPool のロックを回避しつつ、テスト環境のクリーンアップも強化されています。これらの変更は既存テストコードへの影響を最小化し、今後のテスト拡張でも同様のアプローチが適用可能です。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
b9fcf3e0

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文・背景・技術的変更・設計判断・まとめがすべて揃っており、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ⚠ WARNING

シンタックスハイライト・GitHubリンク記法の正確性

コードブロックは正しい `言語:ファイルパス` 形式ですが、PRリンクの記法が `[PR #229](URL)` となっており、仕様の `[#123](URL)` 形式と完全に一致していません。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

専門的な用語が中心で、エンジニア向けの内容になっています。初心者向けの過度な説明はありません。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションは総論・各論・結論の構成が取られており、段落はトピックセンテンスで始まり、1段落1トピック・6文以内に収まっています。空行で区切りも適切です。

Diff内容との照合 ⚠ WARNING

コードブロックとDiff内容の一致

`WebViewNavigationSimulator.swift` のコードブロックは `decisionHandler(.cancel)` の置換のみ示しており、Diffで追加された条件分岐(`if shouldResumeContinuation` 〜 `else` 部分)が省略されています。内容は説明に含まれていますが、コード引用が不完全です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

WKProcessPool、WKNavigationAction、decisionHandler(.cancel) などの用語は正確に使われています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

変更の目的や効果(ロック回避、テスト時間短縮)が PR の説明と合致しています。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

全ての主張は PR のサマリや Diff に裏付けられており、根拠のない推測は見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

テスト件数208件、実行時間約1018秒→約52秒、約95%短縮、12分以上の停滞など、数値は PR のデータと一致しています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルは PR タイトルの日本語訳であり、内容と一致しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

バージョンやLTS等、PR に記載されていない外部知識は含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現は PR の記述(12+ 分、約95% 短縮)と一致しています。