You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 08:00:17