团队在云端 Mac 上跑 iOS 回归测试时,常会让应用依次打开两个 Web 会话:先登录工作空间 A,再切换到工作空间 B。如果 B 读到了 A 的 Cookie,界面看起来可能仍能正常加载,直到用户切换身份才暴露问题。与其只检查“页面能打开”,不如先把网站数据存储的边界写成一条可重复执行的测试。
先确定要隔离的是什么
WKWebView 通过 WKWebViewConfiguration.websiteDataStore 选择网站数据存储。WKWebsiteDataStore.default() 对应持久数据;每次调用 WKWebsiteDataStore.nonPersistent() 可创建一个临时存储实例。本文先验证两个独立临时存储之间的 Cookie 隔离,不把它当作整个登录流程已经安全的证明。
建测试前,在应用代码中找出创建 Web 视图的位置,记录每个视图绑定的存储。若产品要求同一身份的多个页面共享会话,它们就不应误用彼此独立的临时存储;若产品要求切换身份后重新开始,则要确认新视图没有继续引用旧存储。把“哪些页面共享、哪些页面分开”写成表,比事后猜测 Cookie 来源可靠。
WKProcessPool不是这里的验收对象。不要仅凭进程池是否相同,就断言网站数据已经共享或隔离;应检查实际绑定的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 视图复测。清理前限定要删除的数据范围;不要为让测试变绿,就无差别删除设备上其他页面可能仍需使用的记录。
在云端 Mac 上保持结果可复现
让同一个测试 target 在开发桌面和自动化任务里执行,并分别保留测试结果。运行前固定项目要求的 Xcode、模拟器系统版本和测试配置;这里无需假设某个系统版本一定适用。临时存储在每个测试方法中重新创建,测试 Cookie 使用专用名称,避免不同用例互相借用状态。
若出现“单独运行通过、整套运行失败”,先查三件事:测试是否偷偷改用了 default();两个 Web 视图是否复用了同一个存储实例;异步写入、页面导航和断言是否按完成回调排序。不要先靠增加等待秒数掩盖问题。需要复盘时,保存失败用例名称、所用 Xcode 与模拟器版本,以及测试结果文件;日志中不要输出真实会话值。
交付前的检查项
| 检查项 | 通过标准 |
|---|---|
| 存储归属 | 每个 Web 视图的 websiteDataStore 与预期会话一致 |
| 临时隔离 | A 写入的测试 Cookie 在独立的 B 中不可见 |
| 回调顺序 | 写入完成后再读 A,最后读 B |
| 注销验证 | 持久数据与服务端会话分别按产品规则检查 |
| 失败证据 | 记录环境和断言,不记录真实凭据 |
这套测试最适合作为边界测试:先证明本地数据没有跨临时存储流动,再用页面级用例验证身份切换与注销。两层结论分开记录,失败时才知道该查存储绑定、异步时序,还是应用自身的会话逻辑。
常见问题
临时 WKWebsiteDataStore 会自动清除应用的持久 Cookie 吗?
不会。nonPersistent() 创建的临时存储不负责清理 default() 持久存储;注销流程必须针对实际使用的存储单独验收。
两个 WKWebView 使用不同的 WKProcessPool 就足以隔离 Cookie 吗?
不能只凭进程池判断。测试应检查各视图实际绑定的 WKWebsiteDataStore,并在需要时验证页面请求与脚本观察到的状态。
需要一台可远程连接的独享物理 Mac?
核对 M4 Start、M4 Work 与 M4 Pro 的配置、节点和计费周期,再选择适合任务的方案。