单元测试:如何覆盖类中调用Singleton的代码行?
单元测试覆盖Singleton调用的解决方案
问题场景
我需要测试ClassToTest类中对单例MySingleton的调用,当前Xcode测试能通过,但SonarQube提示未覆盖到MySingleton.shared.sendAnalytics("params")这行代码,该如何处理?
待测试类代码
class ClassToTest (){ var testMessage = "" init(){ // 空初始化方法 } func funcWhichHasASingleton() { testMessage = "Function was covered" MySingleton.shared.sendAnalytics("params") } }
现有测试代码
func testExample() { // Given let sut = ClassToTest() // When sut.funcWhichHasASingleton() // Then XCTAssertEqual(sut.testMessage, "Function was covered") }
解决方案
要覆盖并验证单例调用,核心是解耦单例依赖,让测试能控制或验证单例的行为,以下是几种可行方案:
方案1:通过依赖注入解耦(推荐)
修改ClassToTest,将单例依赖通过初始化方法或属性注入,而非直接硬编码调用MySingleton.shared:
首先定义抽象协议,解耦具体实现:
// 抽象统计服务的行为 protocol AnalyticsService { func sendAnalytics(_ params: String) } // 让原有单例遵循协议 class MySingleton: AnalyticsService { static let shared = MySingleton() private init() {} func sendAnalytics(_ params: String) { // 原有业务逻辑实现 } }
修改ClassToTest,依赖协议而非具体单例,同时保留默认使用单例的逻辑,不影响原有业务代码:
class ClassToTest { var testMessage = "" private let analyticsService: AnalyticsService // 默认注入单例,业务代码无需修改 init(analyticsService: AnalyticsService = MySingleton.shared) { self.analyticsService = analyticsService } func funcWhichHasASingleton() { testMessage = "Function was covered" // 调用协议方法,而非直接调用单例 analyticsService.sendAnalytics("params") } }
测试时使用Mock类记录调用行为:
// Mock类,用于验证调用情况 class MockAnalyticsService: AnalyticsService { var sendAnalyticsCalled = false var receivedParams: String? func sendAnalytics(_ params: String) { sendAnalyticsCalled = true receivedParams = params } } func testFuncWhichHasASingleton() { // Given let mockAnalytics = MockAnalyticsService() let sut = ClassToTest(analyticsService: mockAnalytics) // When sut.funcWhichHasASingleton() // Then XCTAssertEqual(sut.testMessage, "Function was covered") // 验证统计方法是否被调用,参数是否正确 XCTAssertTrue(mockAnalytics.sendAnalyticsCalled) XCTAssertEqual(mockAnalytics.receivedParams, "params") }
这种方式既覆盖了原有的单例调用代码,又能验证其行为逻辑,同时解耦了依赖,提升代码可维护性。
方案2:给单例添加测试钩子(快速适配)
如果不想修改ClassToTest的结构,可以给MySingleton添加测试专用的实例替换入口,仅在测试环境生效:
class MySingleton { static var shared: MySingleton = MySingleton() private init() {} func sendAnalytics(_ params: String) { // 原有业务逻辑实现 } // 测试专用:替换单例实例 static func setSharedInstance(_ instance: MySingleton) { shared = instance } }
测试时创建Mock子类替换原单例:
class MockMySingleton: MySingleton { var sendAnalyticsCalled = false var receivedParams: String? override func sendAnalytics(_ params: String) { sendAnalyticsCalled = true receivedParams = params } } func testFuncWhichHasASingleton() { // Given let mockSingleton = MockMySingleton() MySingleton.setSharedInstance(mockSingleton) let sut = ClassToTest() // When sut.funcWhichHasASingleton() // Then XCTAssertEqual(sut.testMessage, "Function was covered") XCTAssertTrue(mockSingleton.sendAnalyticsCalled) XCTAssertEqual(mockSingleton.receivedParams, "params") // 测试结束后恢复原单例,避免影响其他测试用例 MySingleton.setSharedInstance(MySingleton()) }
这种方法无需修改待测试类,仅需给单例增加少量测试代码,适合快速适配现有项目。
方案3:使用第三方测试框架(如OCMock)
如果项目允许引入第三方测试框架,比如OCMock,可以直接Mock单例实例来验证调用:
import OCMock func testFuncWhichHasASingleton() { // Given let mockSingleton = OCMClassMock(MySingleton.self) // Stub单例的shared方法,返回Mock实例 OCMStub(mockSingleton.shared).andReturn(mockSingleton) let sut = ClassToTest() // When sut.funcWhichHasASingleton() // Then XCTAssertEqual(sut.testMessage, "Function was covered") // 验证sendAnalytics方法是否被调用,参数是否匹配 OCMVerify([mockSingleton sendAnalytics:@"params"]) // 清理Mock实例 mockSingleton.stopMocking() }
这种方式无需修改业务代码,但需要引入第三方依赖,适合已使用相关测试框架的项目。
内容的提问来源于stack exchange,提问作者Javier Landa-Torres
相关产品推荐
相关产品推荐

