クラウドMacでiOSの回帰テストを実行するチームでは、アプリでWebセッションを二つ続けて開くことがあります。まずワークスペースAにログインし、次にワークスペースBへ切り替える、という流れです。BがAのCookieを読み取っていても、画面は一見正常に表示され、ユーザーがアカウントを切り替えるまで問題が表面化しないかもしれません。「ページが開く」ことだけを確認するより、Webサイトデータストアの境界を繰り返し検証できるテストにしておきましょう。
何を分離するのかを先に決める
WKWebView は WKWebViewConfiguration.websiteDataStore でWebサイトデータストアを選択します。WKWebsiteDataStore.default() は永続データ用です。WKWebsiteDataStore.nonPersistent() は呼び出すたびに一時ストアのインスタンスを作成できます。ここではまず、独立した二つの一時ストア間でCookieが分離されることを検証します。これをもってログインフロー全体の安全性が証明されたとはみなしません。
テストを作る前に、アプリのコードでWebビューを生成している箇所を探し、各ビューに紐付くストアを記録します。同じアカウントの複数ページでセッションを共有する仕様なら、互いに独立した一時ストアを誤って使うべきではありません。アカウント切り替え後に新しいセッションを開始する仕様なら、新しいビューが古いストアを参照し続けていないことを確認します。「どのページが共有し、どのページを分けるか」を表にしておく方が、後からCookieの出所を推測するより確実です。
WKProcessPoolはここでの合否判定対象ではありません。プロセスプールが同じかどうかだけでWebサイトデータの共有・分離を断定せず、実際に紐付けられたwebsiteDataStoreを確認してください。
外部ネットワークに依存しないストアテストを書く
アプリのユニットテストtargetに WebKit と XCTest を導入します。次のテストはCookieストアだけを操作し、ネットワークリクエストを行いません。テストサーバーは不要で、ページの読み込み速度にも左右されません。test.invalid はサンプルCookieのドメインであり、サービスの接続先ではありません。
import WebKit
import XCTest
final class WebsiteDataIsolationTests: XCTestCase {
func testEphemeralStoresDoNotShareCookie() {
let storeA = WKWebsiteDataStore.nonPersistent()
let storeB = WKWebsiteDataStore.nonPersistent()
let done = expectation(description: "Both stores checked")
guard let cookie = HTTPCookie(properties: [
.domain: "test.invalid",
.path: "/",
.name: "isolation_probe",
.value: "only_a"
]) else {
XCTFail("Could not create probe cookie")
return
}
storeA.httpCookieStore.setCookie(cookie) {
storeA.httpCookieStore.getAllCookies { cookiesA in
XCTAssertTrue(cookiesA.contains {
$0.name == "isolation_probe" && $0.value == "only_a"
})
storeB.httpCookieStore.getAllCookies { cookiesB in
XCTAssertFalse(cookiesB.contains {
$0.name == "isolation_probe"
})
done.fulfill()
}
}
}
wait(for: [done], timeout: 5)
}
}
まずAで書き込んだ値を読み取れることを確かめ、次にBでは同名のCookieを読み取れないことを確かめます。この順序は重要です。書き込み完了前にBを読み取ると、誤ってデータを共有していてもテストが「成功」する可能性があります。タイムアウトはコールバックの連鎖が完了しなかったことを示します。制限時間を際限なく延ばすのではなく、失敗として原因を調べてください。
ストアを実際のページに接続する
ストア層のテストが通ったら、アプリでのビュー生成方法を確認します。設定は WKWebView を初期化する前に行う必要があります。作成済みのビューとは別の設定を後から変更しても、そのビューのストアは切り替わりません。ページテストのセットアップでは、対象のインスタンスを明示的に渡せます。
let sessionStore = WKWebsiteDataStore.nonPersistent()
let configuration = WKWebViewConfiguration()
configuration.websiteDataStore = sessionStore
let webView = WKWebView(frame: .zero, configuration: configuration)
続く統合テストでは、管理下にあるテストページを読み込み、ログイン、状態の読み取り、アカウント切り替え、ログアウトをページに実行させてから、リクエストと画面表示を確認します。ストアAPIで証明できるのは、テスト用Cookieが二つのインスタンス間で見えないことです。それだけではサーバー側のセッションが無効になったことも、アプリがネイティブ層からトークンをページへ再注入していないことも証明できません。実際のリクエストが必要なら、チームが管理するテスト用サービスを使い、ネットワーク障害とセッションに関するアサーションを分けて記録します。
永続ストアは別のテスト対象
nonPersistent() のテストが通ったからといって、ログアウト処理が正しいとは判断しないでください。アプリが default() を使用する場合、ログアウトテストでは、該当する永続ストア内の関連するWebサイトデータが製品のルールどおりに処理されるかを確認し、Webビューを作り直して再テストします。削除前に対象データの範囲を限定してください。テストを通すために、デバイス上の別のページがまだ必要とするかもしれないデータまで無差別に削除してはいけません。
クラウドMacで再現可能な結果を得る
同じテストtargetを開発用デスクトップと自動化ジョブの両方で実行し、それぞれのテスト結果を保存します。実行前に、プロジェクトで指定されたXcode、シミュレータのOSバージョン、テスト設定を固定します。ここで特定のOSバージョンが必ず適していると仮定する必要はありません。各テストメソッドで一時ストアを作り直し、テスト用Cookieには専用の名前を使って、テストケース間で状態を流用しないようにします。
「単独実行では通るのに、テストスイート全体では失敗する」場合は、まず三つ確認します。テストが意図せず default() を使っていないか、二つのWebビューが同じストアのインスタンスを再利用していないか、非同期書き込み・ページ遷移・アサーションが完了コールバックに従って順序付けられているか、です。最初から待機時間を延ばして問題を覆い隠さないでください。後で調査できるよう、失敗したテストケース名、使用したXcodeとシミュレータのバージョン、テスト結果ファイルを保存します。ログに実際のセッション値を出力してはいけません。
引き渡し前のチェック項目
| チェック項目 | 合格基準 |
|---|---|
| ストアの紐付け | 各Webビューの websiteDataStore が想定するセッションと一致する |
| 一時ストアの分離 | Aに書き込んだテスト用Cookieが、独立したBからは見えない |
| コールバックの順序 | 書き込み完了後にAを読み、最後にBを読む |
| ログアウトの検証 | 永続データとサーバー側セッションを、それぞれ製品のルールに従って確認する |
| 失敗時の証拠 | 環境とアサーションを記録し、実際の認証情報は記録しない |
このテストは境界テストとして使うのが最適です。まずローカルデータが一時ストア間を移動しないことを証明し、次にページ単位のテストでアカウント切り替えとログアウトを検証します。二層の結論を分けて記録しておけば、失敗時にストアの紐付け、非同期処理の順序、アプリ自身のセッションロジックのどこを調べるべきか判断できます。
よくある質問
一時データストアは永続Cookieも削除しますか?
いいえ。nonPersistent()はdefault()を消去しません。ログアウト時には、実際に使った永続ストアを別途検証します。
WKProcessPoolを分ければCookieも分離されますか?
それだけでは判断できません。各WebビューのWKWebsiteDataStoreを確認し、必要に応じて実際のページ要求もテストしてください。
リモート接続できる専有物理Macをお探しですか?
M4 Start、M4 Work、M4 Proのスペック、ロケーション、課金期間を比較して、用途に合ったプランをお選びください。