iOS Swift如何为ViewController的代理方法编写正确单元测试用例
代理方法单元测试实现方案
核心测试逻辑
我们需要验证代理方法触发后的三个核心行为是否符合预期:
- 方法调用后确实弹出了
PremiumCustomNavigationController实例 - 弹出的导航容器内的
PremiumFeedbackViewController的id属性被正确赋值为当前VC的fileDetails.id - 异常场景(找不到对应Storyboard控制器、依赖缺失)下不会执行错误逻辑
完整测试代码实现
第一步:先定义测试需要的Mock类,用于捕获页面跳转行为
import UIKit @testable import 你的项目主模块名 // 用于捕获present行为的Mock控制器 class MockPresentingViewController: UIViewController { /// 记录被弹出的控制器 var presentedTargetVC: UIViewController? /// 记录present方法是否被调用 var isPresentTriggered = false override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) { isPresentTriggered = true presentedTargetVC = viewControllerToPresent // 不需要真的执行跳转逻辑,只需要记录行为即可 } } // 用于替换原协调器的Mock类 class MockDecisionTreeCoordinator: EMPDecisionTreeCoordinator { /// 替换原rootViewController为我们的Mock实例 var mockRoot: MockPresentingViewController! override var rootViewController: UIViewController { return mockRoot } }
第二步:编写完整的测试类
import XCTest @testable import 你的项目主模块名 class DownloadBaseViewControllerTests: XCTestCase { var downloadBaseViewController: DownloadBaseViewController! var mockPresentVC: MockPresentingViewController! var mockCoordinator: MockDecisionTreeCoordinator! // 测试用的模拟ID let testFileId = 10086 override func setUpWithError() throws { // 1. 从Storyboard正确初始化目标VC,替换成你实际的Storyboard名称和VC标识符 let storyboard = UIStoryboard(name: "Main", bundle: Bundle(for: DownloadBaseViewController.self)) downloadBaseViewController = storyboard.instantiateViewController(withIdentifier: "DownloadBaseViewController") as? DownloadBaseViewController // 触发视图加载,避免视图相关属性为nil downloadBaseViewController.loadViewIfNeeded() // 2. 赋值测试用的fileDetails,替换成你实际的模型构造方法 downloadBaseViewController.fileDetails = FileDetails(id: testFileId) // 3. 初始化Mock依赖,替换原VC的协调器 mockPresentVC = MockPresentingViewController() mockCoordinator = MockDecisionTreeCoordinator() mockCoordinator.mockRoot = mockPresentVC // 原代码是强制解包coordinator,这里必须赋值否则测试会崩溃 downloadBaseViewController.decesiontreeCoordinator = mockCoordinator } override func tearDownWithError() throws { // 测试后清空所有实例避免影响其他用例 downloadBaseViewController = nil mockPresentVC = nil mockCoordinator = nil } func testDecisionEmptyTreeFeedbackButtonTapped() throws { // 调用待测试的代理方法 downloadBaseViewController.decisionEmptyTreeFeedbackButtonTapped() // 断言1:present方法被正确调用 XCTAssertTrue(mockPresentVC.isPresentTriggered, "反馈页面的弹出逻辑未触发") // 断言2:弹出的控制器是PremiumCustomNavigationController类型 guard let navVC = mockPresentVC.presentedTargetVC as? PremiumCustomNavigationController else { XCTFail("未正确实例化PremiumCustomNavigationController") return } // 断言3:导航容器内的第一个控制器是PremiumFeedbackViewController,且id正确赋值 guard let feedbackVC = navVC.children.first as? PremiumFeedbackViewController else { XCTFail("导航容器内未包含正确的反馈控制器") return } XCTAssertEqual(feedbackVC.id, testFileId, "反馈控制器的id属性未被正确赋值") } }
代码说明
- Mock类的作用是剥离对真实业务组件的依赖,只关心目标方法的行为是否符合预期,不需要依赖真实的导航栈、协调器逻辑
- 初始化阶段必须正确从Storyboard实例化目标VC,否则
storyboard属性会为nil,方法内的控制器实例化逻辑永远不会执行 - 如果需要覆盖异常场景,可以单独写测试用例,比如将
fileDetails设为nil、删除Storyboard里的对应控制器标识符,验证不会触发present逻辑
内容的提问来源于stack exchange,提问作者iOSDude
相关产品推荐
相关产品推荐

