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

带默认实现的协议扩展如何便捷测试?

嘿,这个场景我太熟悉了!之前从构造注入转成协议扩展的时候,也纠结过怎么优雅地测试这种依赖系统Bundle的逻辑,给你分享几个实用的方案:

方案1:直接传入测试用自定义Bundle

你已经在方法里预留了bundle参数,这是测试的关键!我们可以创建一个仅包含测试所需信息的Mock Bundle,无需依赖Bundle.main。

示例代码

首先是你的业务代码:

// 定义协议和扩展
protocol AppInfoCapable {}

extension AppInfoCapable {
    func build(bundle: Bundle = .main) -> String {
        return bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "unknown"
    }
}

// 你的Foo类遵循协议
class Foo: AppInfoCapable {}

然后是测试代码:

import XCTest
@testable import YourApp

class FooTests: XCTestCase {
    func testBuild_WithTestBundle_ReturnsExpectedVersion() {
        // 1. 创建包含测试数据的Info字典
        let testInfoDict = ["CFBundleVersion": "1.2.3-test"]
        
        // 2. 初始化一个空Bundle,替换其类为自定义TestBundle
        let testBundle = Bundle(path: "/dev/null")!
        object_setClass(testBundle, TestBundle.self)
        
        // 3. 给TestBundle设置测试用的Info字典
        (testBundle as! TestBundle).mockInfoDictionary = testInfoDict
        
        // 4. 执行测试
        let foo = Foo()
        let result = foo.build(bundle: testBundle)
        
        XCTAssertEqual(result, "1.2.3-test")
    }
}

// 自定义Bundle子类,用于替换默认的infoDictionary逻辑
class TestBundle: Bundle {
    var mockInfoDictionary: [String: Any]?
    
    override var infoDictionary: [String: Any]? {
        return mockInfoDictionary
    }
}
方案2:用协议包装Bundle(扩展性更强)

如果项目里很多地方都依赖Bundle,建议封装一个BundleProvider协议,让协议扩展依赖这个抽象,而不是直接依赖Bundle类,这样测试时可以轻松替换实现。

示例代码

业务代码部分:

// 定义Bundle抽象协议
protocol BundleProviding {
    var appBundle: Bundle { get }
}

// 默认实现,指向真实的Bundle.main
class DefaultBundleProvider: BundleProviding {
    var appBundle: Bundle { .main }
}

// 修改协议扩展,依赖BundleProviding
protocol AppInfoCapable {}

extension AppInfoCapable {
    func build(provider: BundleProviding = DefaultBundleProvider()) -> String {
        return provider.appBundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "unknown"
    }
}

class Foo: AppInfoCapable {}

测试代码部分:

class MockBundleProvider: BundleProviding {
    var mockBundle: Bundle!
    
    var appBundle: Bundle {
        return mockBundle
    }
}

class FooTests: XCTestCase {
    func testBuild_WithMockProvider_ReturnsExpectedVersion() {
        // 创建测试Bundle
        let testInfoDict = ["CFBundleVersion": "4.5.6-mock"]
        let testBundle = Bundle(path: "/dev/null")!
        object_setClass(testBundle, TestBundle.self)
        (testBundle as! TestBundle).mockInfoDictionary = testInfoDict
        
        // 配置MockProvider
        let mockProvider = MockBundleProvider()
        mockProvider.mockBundle = testBundle
        
        // 执行测试
        let foo = Foo()
        let result = foo.build(provider: mockProvider)
        
        XCTAssertEqual(result, "4.5.6-mock")
    }
}
方案3:复用测试Target的Bundle

如果只是简单验证逻辑,也可以直接使用测试Target自身的Bundle——只要你在测试Target的Info.plist里配置好对应字段(比如CFBundleVersion),就能快速完成测试:

class FooTests: XCTestCase {
    func testBuild_WithTestTargetBundle_ReturnsTestVersion() {
        let testBundle = Bundle(for: FooTests.self)
        let foo = Foo()
        let result = foo.build(bundle: testBundle)
        
        // 对应测试Target Info.plist里的CFBundleVersion值
        XCTAssertEqual(result, "test-1.0")
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.19 10:34:06