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

Swift单元测试:如何Mock注入UIView的ApiProtocol方法?

最优实现方案:依赖注入 + 测试专属Mock类

你的问题核心在于MyUIView直接绑定了ApiProtocol的默认实现,导致测试时没法灵活替换stubMePlease()的返回值。最优雅的解法是通过依赖注入把ApiProtocol从MyUIView内部抽离出来,然后在每个测试用例里用自定义Mock来返回指定值,既解耦又灵活。

第一步:重构MyUIView,引入依赖注入

我们把ApiProtocol做成MyUIView的初始化参数,生产环境用默认实现,测试时可以替换成Mock:

protocol ApiProtocol { func stubMePlease() -> String }
extension ApiProtocol {
    func stubMePlease() -> String { return "hello world" }
}

class MyUIView: UIView {
    private let api: ApiProtocol
    
    // 新增带api参数的初始化方法,默认用默认实现,不影响生产代码调用
    init(frame: CGRect, api: ApiProtocol = DefaultApi()) {
        self.api = api
        super.init(frame: frame)
        foo()
    }
    
    required init?(coder aDecoder: NSCoder) {
        self.api = DefaultApi()
        super.init(coder: aDecoder)
    }
    
    // 现在调用注入的api实例的方法,而不是self的实现
    public func foo() -> String {
        return api.stubMePlease()
    }
}

// 专门的默认实现类,用于生产环境
class DefaultApi: ApiProtocol {}

第二步:测试中用通用Mock类快速配置返回值

我们写一个通用的Mock类,初始化时就能指定返回值,这样每个测试用例可以轻松生成符合要求的Mock实例:

import XCTest
class MyUIViewTests: XCTestCase {
    // 通用Mock类,支持自定义返回值
    class MockApi: ApiProtocol {
        private let stubResult: String
        init(stubResult: String) {
            self.stubResult = stubResult
        }
        func stubMePlease() -> String {
            return stubResult
        }
    }
    
    override func setUp() {
        super.setUp()
        // 这里不再统一初始化,每个测试用例自己创建带指定Mock的实例
    }
    
    func testA() {
        let myview = MyUIView(frame: .zero, api: MockApi(stubResult: "hello"))
        XCTAssertEqual(myview.foo(), "hello")
    }
    
    func testB() {
        let myview = MyUIView(frame: .zero, api: MockApi(stubResult: "weekend"))
        XCTAssertEqual(myview.foo(), "weekend")
    }
    
    func testC() {
        let myview = MyUIView(frame: .zero, api: MockApi(stubResult: "swift"))
        XCTAssertEqual(myview.foo(), "swift")
    }
}

为什么这个方案最优?

  • 解耦彻底:把业务逻辑和UI组件分开,符合单一职责原则,后续维护更方便
  • 测试独立:每个测试用例的Mock配置互不干扰,不会出现测试污染
  • 生产无侵入:默认初始化逻辑和原来完全一致,不需要修改任何生产代码的调用方式
  • 扩展性强:以后ApiProtocol加新方法,只需要更新Mock类,测试逻辑不用大改

备选临时方案:子类Mock(不推荐)

如果暂时不想重构依赖注入,也可以给MyUIView写子类重写方法,但这种方式代码冗余,耦合度高,只适合临时场景:

func testA() {
    class MockView: MyUIView {
        override func stubMePlease() -> String {
            return "hello"
        }
    }
    let myview = MockView()
    XCTAssertEqual(myview.foo(), "hello")
}

但每个测试都要写一个子类,代码重复率高,而且如果MyUIView初始化逻辑复杂,子类可能会引入额外问题,所以还是依赖注入的方案更靠谱。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 06:55:57