如何在XCTest运行时获取应用的所有UI交互操作?
获取XCTest UI交互操作的几种可行方案
刚好之前我也折腾过类似的需求——想要在XCTest运行时捕获那些自动输出到控制台和TestSummaries.plist的UI交互日志。官方确实没直接提供现成的API,但我们可以通过几种方式实现,下面给你详细说说:
1. 自定义XCTest观察者 + 方法Hook结合
XCTestObservations主要负责监听测试生命周期,但结合方法交换(Swizzling),我们可以把UI操作的日志自动收集到观察者中,实现实时追踪。
第一步:实现自定义测试观察者
这个观察者负责管理测试的交互日志,在测试开始/结束时做初始化和输出:
import XCTest class TestInteractionObserver: NSObject, XCTestObservations { static let shared = TestInteractionObserver() private var currentTestCaseName: String? private var interactionLogs: [String] = [] func testCaseDidStart(_ testCase: XCTestCase) { currentTestCaseName = testCase.name interactionLogs.removeAll() print("\n=== 测试 \(testCase.name) 开始记录交互 ===") } func testCaseDidFinish(_ testCase: XCTestCase) { guard let testName = currentTestCaseName else { return } print("\n=== 测试 \(testName) 交互日志汇总 ===") interactionLogs.forEach { print("- \($0)") } currentTestCaseName = nil } // 对外提供添加日志的方法 func addInteractionLog(_ log: String) { interactionLogs.append(log) } }
第二步:Hook XCUIElement的UI操作方法
通过Runtime交换XCUIElement的默认操作方法(比如tap()、swipeLeft()),让每次操作自动记录日志:
import XCTest import ObjectiveC extension XCUIElement { // 交换tap方法 private static let originalTap = #selector(tap) private static let swizzledTap = #selector(swizzledTapAction) @objc private func swizzledTapAction() { // 生成详细的交互日志,包含元素标识、类型等信息 let log = "点击元素: [类型: \(elementType.rawValue), ID: \(identifier), 文本: \(label)]" TestInteractionObserver.shared.addInteractionLog(log) // 调用原tap方法 swizzledTapAction() } // 统一执行方法交换的入口 static func setupSwizzling() { guard let originalMethod = class_getInstanceMethod(Self.self, originalTap), let swizzledMethod = class_getInstanceMethod(Self.self, swizzledTap) else { return } method_exchangeImplementations(originalMethod, swizzledMethod) } // 你可以用同样的方式hook其他操作,比如滑动、输入文本 // 示例:hook typeText方法 private static let originalTypeText = #selector(typeText(_:)) private static let swizzledTypeText = #selector(swizzledTypeTextAction(_:)) @objc private func swizzledTypeTextAction(_ text: String) { let log = "输入文本: '\(text)' 到元素 [ID: \(identifier), 文本: \(label)]" TestInteractionObserver.shared.addInteractionLog(log) swizzledTypeTextAction(text) } }
第三步:在测试类中初始化
在你的测试基类或者具体测试类的setUp()里注册观察者和启动方法交换:
class BaseUITest: XCTestCase { override func setUp() { super.setUp() // 注册交互观察者 XCTestObservationCenter.shared.addTestObserver(TestInteractionObserver.shared) // 启动方法交换 XCUIElement.setupSwizzling() // 其他初始化配置... } override func tearDown() { XCTestObservationCenter.shared.removeTestObserver(TestInteractionObserver.shared) super.tearDown() } }
2. 运行时读取TestSummaries.plist(事后获取)
如果不需要实时捕获,只是想在测试结束后拿到完整的交互日志,可以动态读取Derived Data中的TestSummaries.plist,解析里面的Activities节点(系统自动记录的交互都存在这里):
func fetchTestSummariesLogs() -> [String]? { guard let derivedDataPath = ProcessInfo.processInfo.environment["DERIVED_DATA_DIR"] else { print("无法获取Derived Data路径") return nil } // 拼接TestSummaries.plist的路径(Xcode 14+路径结构) let summariesPath = "\(derivedDataPath)/Logs/Test/TestSummaries.plist" guard FileManager.default.fileExists(atPath: summariesPath), let plistData = try? Data(contentsOf: URL(fileURLWithPath: summariesPath)), let summaries = try? PropertyListSerialization.propertyList(from: plistData, options: [], format: nil) as? [String: Any], let testableSummaries = summaries["TestableSummaries"] as? [[String: Any]], let firstSummary = testableSummaries.first, let tests = firstSummary["Tests"] as? [[String: Any]] else { return nil } // 提取所有交互日志 var allLogs: [String] = [] for test in tests { if let activities = test["Activities"] as? [[String: Any]] { for activity in activities { if let title = activity["Title"] as? String { allLogs.append(title) } } } } return allLogs }
注意事项
- 方法交换(Swizzling)只建议在测试环境使用,避免影响生产代码逻辑;
- Xcode版本更新可能会改变
TestSummaries.plist的结构,解析时需要对应调整; - 如果需要把日志输出到自定义文件,只需要在观察者的
testCaseDidFinish里把interactionLogs写入文件即可。
内容的提问来源于stack exchange,提问作者Harsh
相关产品推荐
相关产品推荐

