如何在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
相关产品推荐
相关产品推荐

