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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.30 04:06:03