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

如何在Xcode UITest中检测App下拉刷新时的崩溃异常

UI测试无法捕获下拉刷新导致的App崩溃问题

问题描述

尝试编写Xcode UI测试检测UITableView下拉刷新时是否崩溃,运行测试时App实际崩溃(Xcode可见异常),但测试始终显示成功。两种测试写法均无效,示例项目复现了该问题。

测试代码尝试

第一种写法:

func testPullToRefresh_shouldNotCrash() throws {
    do {
        let app = XCUIApplication()
        app.launch()
        
        let tableView = app.tables.firstMatch
        try pullToRefresh(from: tableView)
    } catch {
        XCTFail("Crashed")
    }
}

第二种写法:

func testPullToRefresh_shouldNotCrash() {
    let app = XCUIApplication()
    app.launch()
    
    let tableView = app.tables.firstMatch
    XCTAssertNoThrow(try pullToRefresh(from: tableView))
}

示例项目代码

ViewController

import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!
    
    private var dataSource = DataSource()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.dataSource = dataSource
        tableView.delegate = dataSource
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
        
        let refreshControl = UIRefreshControl()
        refreshControl.addTarget(self, action: #selector(refreshData), for: .valueChanged)
        tableView.refreshControl = refreshControl
    }
    
    @objc func refreshData() throws {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            self.tableView.insertRows(at: [IndexPath(item: 0, section: 1)], with: .automatic)
            self.tableView.refreshControl?.endRefreshing()
        }
    }
}

DataSource

import UIKit

class DataSource: NSObject, UITableViewDataSource, UITableViewDelegate {
    
    var data = ["Cell 1", "Cell 2", "Cell 3", "Cell 4", "Cell 5"]
    
    // UITableViewDataSource
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return data.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        cell.textLabel?.text = data[indexPath.row]
        return cell
    }
    
    // UITableViewDelegate
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
    }
}

测试类

import XCTest

final class ExceptionTestsUITests: XCTestCase {

    func testPullToRefresh_shouldNotCrash() throws {
        do {
            let app = XCUIApplication()
            app.launch()
            
            let tableView = app.tables.firstMatch
            try pullToRefresh(from: tableView)
        } catch {
            XCTFail("Crashed")
        }
    }
    
    func pullToRefresh(from element: XCUIElement) throws {
        let startPosition = CGPoint(x: 100, y: 100)
        let endPosition = CGPoint(x: 100, y: 500)
        let start = element.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0)).withOffset(CGVector(dx: startPosition.x, dy: startPosition.y))
        let finish = element.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0)).withOffset(CGVector(dx: endPosition.x, dy: endPosition.y))
        start.press(forDuration: 0, thenDragTo: finish)
    }
}

问题原因

崩溃发生在异步任务中:refreshData里用DispatchQueue.main.asyncAfter延迟1秒执行插入行操作,而原测试代码执行完拖拽刷新后立即结束,没有等待异步任务完成。崩溃发生在测试已经结束之后,自然无法被XCTFail或XCTAssertNoThrow捕获。

解决方案

要让测试捕获崩溃,必须等待异步刷新任务执行完毕,并检查App的运行状态。

方案1:等待刷新控件状态变化(更可靠)

通过等待刷新控件出现并消失,确保刷新任务执行完成,再检查App是否正常运行:

func testPullToRefresh_shouldNotCrash() {
    let app = XCUIApplication()
    app.launch()
    
    let tableView = app.tables.firstMatch
    // 触发下拉刷新
    pullToRefresh(from: tableView)
    
    // 等待刷新控件出现(超时2秒)
    let refreshControl = tableView.refreshControls.firstMatch
    XCTAssertTrue(refreshControl.waitForExistence(timeout: 2), "Refresh control did not appear")
    
    // 等待刷新控件消失,确认刷新完成(超时2秒)
    XCTAssertFalse(refreshControl.waitForExistence(timeout: 2), "Refresh did not complete")
    
    // 检查App是否仍在前台运行
    XCTAssertEqual(app.state, .runningForeground, "App crashed after pull-to-refresh")
}

方案2:使用XCTestExpectation等待异步任务

通过测试期望(Expectation)等待足够时间,确保异步任务执行完成后再验证状态:

func testPullToRefresh_shouldNotCrash() {
    let app = XCUIApplication()
    app.launch()
    
    let refreshCompleteExpectation = XCTestExpectation(description: "Refresh task completes")
    
    let tableView = app.tables.firstMatch
    pullToRefresh(from: tableView)
    
    // 等待异步刷新任务执行(设置比业务代码更长的超时)
    DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
        refreshCompleteExpectation.fulfill()
    }
    
    // 等待期望完成,超时3秒
    wait(for: [refreshCompleteExpectation], timeout: 3)
    
    // 验证App未崩溃
    XCTAssertEqual(app.state, .runningForeground, "App crashed during pull-to-refresh")
}

额外说明

原业务代码中的崩溃根源是:插入了section:1的行,但DataSource未实现numberOfSections方法(默认返回1),导致数组越界。修复业务代码可解决崩溃,但上述测试方案能有效捕获此类运行时崩溃。

内容的提问来源于stack exchange,提问作者Kamilton

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 03:38:18