如何对在视图控制器外调用rootViewController.present的函数做单元测试?
嘿,我来帮你搞定这个单元测试里的视图弹出函数问题!你遇到的错误本质很明确:UIViewController.present(_:animated:completion:)要求的completion是不带参数的闭包((() -> Void)?),但你自己定义的函数completion是带Bool参数的((Bool) -> Void)?,两者类型不兼容,所以编译器直接报错了。下面给你几个针对性的解决方案,结合单元测试场景来拆解:
方案1:直接调整函数的Completion类型(最简洁)
如果你的弹出函数不需要通过completion传递Bool结果(只是通知弹出完成),那直接把completion改成和系统present一致的类型即可:
// 修改后的弹出函数 func presentTargetVC(from rootVC: UIViewController?, completion: (() -> Void)? = nil) { let targetVC = YourTargetViewController() rootVC?.present(targetVC, animated: true, completion: completion) }
对应的单元测试可以用XCTestExpectation来验证completion是否被触发,同时验证弹出的VC是否正确:
func testPresentTargetVC_Success() { // 创建测试期望,用于等待completion回调 let completionExpectation = XCTestExpectation(description: "Present completion triggered") let mockRootVC = UIViewController() // 调用弹出函数 presentTargetVC(from: mockRootVC) { completionExpectation.fulfill() } // 等待回调触发,超时时间1秒足够UI操作完成 wait(for: [completionExpectation], timeout: 1.0) // 额外验证:确认rootVC确实弹出了目标VC XCTAssertTrue(mockRootVC.presentedViewController is YourTargetViewController) }
方案2:保留Bool参数的Completion(需要传递结果时)
如果你的业务逻辑需要通过completion告知调用者“弹出是否成功”(比如判断rootVC是否存在),那可以在系统present的completion里做一层包装,把Bool结果传递出去:
// 带Bool结果的弹出函数 func presentTargetVC(from rootVC: UIViewController?, completion: ((Bool) -> Void)? = nil) { guard let validRootVC = rootVC else { // 没有可用的rootVC,直接返回失败 completion?(false) return } let targetVC = YourTargetViewController() validRootVC.present(targetVC, animated: true) { // present完成后,返回成功状态 completion?(true) } }
对应的单元测试要分成功和失败两种场景:
func testPresentTargetVC_Success() { let expectation = XCTestExpectation(description: "Present success callback triggered") let mockRootVC = UIViewController() presentTargetVC(from: mockRootVC) { success in // 验证回调返回成功 XCTAssertTrue(success) expectation.fulfill() } wait(for: [expectation], timeout: 1.0) XCTAssertTrue(mockRootVC.presentedViewController is YourTargetViewController) } func testPresentTargetVC_Failure() { let expectation = XCTestExpectation(description: "Present failure callback triggered") // 传入nil的rootVC,模拟失败场景 presentTargetVC(from: nil) { success in XCTAssertFalse(success) expectation.fulfill() } wait(for: [expectation], timeout: 1.0) }
方案3:用MockViewController做精准测试(进阶)
如果想更严谨地验证present方法是否被正确调用(而不仅仅依赖completion),可以创建一个MockViewController类来记录present的调用情况:
class MockRootViewController: UIViewController { // 记录是否调用了present方法 var wasPresentCalled = false // 记录被弹出的VC实例 var presentedViewControllerInstance: UIViewController? override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) { wasPresentCalled = true presentedViewControllerInstance = viewControllerToPresent // 手动触发completion,模拟系统行为 completion?() } }
然后在单元测试里使用这个Mock类:
func testPresentTargetVC_WithMock() { let expectation = XCTestExpectation(description: "Completion callback triggered") let mockRootVC = MockRootViewController() presentTargetVC(from: mockRootVC) { success in XCTAssertTrue(success) expectation.fulfill() } wait(for: [expectation], timeout: 1.0) // 验证present方法确实被调用 XCTAssertTrue(mockRootVC.wasPresentCalled) // 验证弹出的是目标VC XCTAssertTrue(mockRootVC.presentedViewControllerInstance is YourTargetViewController) }
小提示:UI操作要在主队列执行
单元测试中涉及UIKit的操作,最好放在主队列执行,避免线程问题。可以把测试代码包在DispatchQueue.main.async里,或者用XCTest的perform(on: .main)方法:
func testPresentTargetVC_OnMainQueue() { let expectation = XCTestExpectation(description: "Present completion on main queue") DispatchQueue.main.async { let mockRootVC = UIViewController() self.presentTargetVC(from: mockRootVC) { expectation.fulfill() } } wait(for: [expectation], timeout: 1.0) }
内容的提问来源于stack exchange,提问作者SwiftyJD

