带默认实现的协议扩展如何便捷测试?
嘿,这个场景我太熟悉了!之前从构造注入转成协议扩展的时候,也纠结过怎么优雅地测试这种依赖系统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
相关产品推荐
相关产品推荐

