Swift中静态函数的依赖注入与单元测试实现方案问询
解决静态网络调用的依赖注入与单元测试问题
我完全懂你的困扰——静态调用确实会把代码死死耦合在一起,让单元测试无从下手。咱们一步步来拆解,把这些静态依赖都换成可注入的协议,这样就能轻松Mock各个组件,写出靠谱的单元测试了。
第一步:把静态依赖抽象成协议
我们需要把每个静态调用的类都抽象成协议,这样就能用Mock类替换真实实现。
1. 抽象网络调用(ServiceCaller)
先给ServiceCaller定义一个协议,把核心的performRequest方法放进去:
protocol ServiceCalling { func performRequest(route: ServiceRouter, completion: @escaping (ResultType) -> Void) // 注意:这里的ResultType要和你项目中真实的结果类型一致(比如包含data、response、error的结构体) } // 让原来的ServiceCaller遵循这个协议,把静态方法改成实例方法 class ServiceCaller: ServiceCalling { // 如果项目里大量用了静态调用,可以保留单例作为过渡 static let shared = ServiceCaller() func performRequest(route: ServiceRouter, completion: @escaping (ResultType) -> Void) { // 这里放原来静态方法里的网络请求逻辑 } }
2. 抽象错误处理(ServiceError)
同样给错误处理抽象协议:
protocol ServiceErrorHandling { func handle(error: Error, statusCode: Int) -> Error // 返回的Error类型要和你项目中的自定义错误类型一致 } class ServiceError: ServiceErrorHandling { static let shared = ServiceError() func handle(error: Error, statusCode: Int) -> Error { // 原来静态handle方法的错误处理逻辑 } }
3. 抽象URL缓存(URLCache)
对URL缓存的单例也做同样处理:
protocol URLCaching { func getServiceURL(key: URLKeys) -> String } class URLCache: URLCaching { static let instance = URLCache() // 保留原来的单例命名 func getServiceURL(key: URLKeys) -> String { // 原来的URL获取逻辑 } }
第二步:改造AppInitService,注入依赖
现在把AppInitService从静态类改成实例类,把上面三个协议作为依赖注入进去:
class AppInitService { private let serviceCaller: ServiceCalling private let errorHandler: ServiceErrorHandling private let urlCache: URLCaching // 初始化方法注入所有依赖,同时提供默认值兼容原有代码 init( serviceCaller: ServiceCalling = ServiceCaller.shared, errorHandler: ServiceErrorHandling = ServiceError.shared, urlCache: URLCaching = URLCache.instance ) { self.serviceCaller = serviceCaller self.errorHandler = errorHandler self.urlCache = urlCache } // 把原来的静态方法改成实例方法 func initAppRequest(_ completion: @escaping (_ appInitRecevingModel: Result<AppInitRecevingModel>) -> Void) { let sendingModel = AppInitSendingModel(cmsVersion: AppDefaults.instance.getCMSVersion()) let route = ServiceRouter( method: .post, path: urlCache.getServiceURL(key: URLKeys.initApp), // 用注入的urlCache代替单例 parameters: sendingModel.getJSONData(), timeoutSec: 1 ) // 用注入的serviceCaller代替静态调用 serviceCaller.performRequest(route: route) { [weak self] result in guard let self = self else { return } if let error = result.error { let statusCode = result.response?.statusCode ?? error._code let handledError = self.errorHandler.handle(error: error, statusCode: statusCode) // 用注入的errorHandler completion(.error(handledError)) } else { guard let data = result.data else { let emptyError = self.errorHandler.handle(error: NSError(domain: "EmptyData", code: -1003, userInfo: nil), statusCode: -1003) completion(.error(emptyError)) return } do { var responseJson = JSON(data) responseJson["idleTimeoutInMinutes"] = 10 let input = try AppInitRecevingModel(data: responseJson.rawData()) completion(.success(input)) } catch let error { let handledError = self.errorHandler.handle(error: error, statusCode: -1002) completion(.error(handledError)) } } } } }
第三步:编写Mock类用于测试
现在我们可以为每个协议编写Mock类,完全控制测试中的返回结果:
Mock ServiceCaller
class MockServiceCaller: ServiceCalling { // 记录是否调用了performRequest var didCallPerformRequest = false // 自定义要返回的结果 var resultToReturn: ResultType! func performRequest(route: ServiceRouter, completion: @escaping (ResultType) -> Void) { didCallPerformRequest = true completion(resultToReturn) } }
Mock ServiceErrorHandler
class MockErrorHandler: ServiceErrorHandling { var didCallHandleError = false var errorToReturn: Error! func handle(error: Error, statusCode: Int) -> Error { didCallHandleError = true return errorToReturn } }
Mock URLCache
class MockURLCache: URLCaching { var urlToReturn = "/mock-init-app" func getServiceURL(key: URLKeys) -> String { return urlToReturn } }
第四步:编写单元测试
现在可以在测试类里注入这些Mock,测试不同场景:
class MyProjectAppInitTests: XCTestCase { var appInitService: AppInitService! var mockServiceCaller: MockServiceCaller! var mockErrorHandler: MockErrorHandler! var mockURLCache: MockURLCache! override func setUp() { super.setUp() // 初始化所有Mock mockServiceCaller = MockServiceCaller() mockErrorHandler = MockErrorHandler() mockURLCache = MockURLCache() // 注入Mock到AppInitService appInitService = AppInitService( serviceCaller: mockServiceCaller, errorHandler: mockErrorHandler, urlCache: mockURLCache ) } override func tearDown() { appInitService = nil mockServiceCaller = nil mockErrorHandler = nil mockURLCache = nil super.tearDown() } // 测试成功返回数据的场景 func testInitAppRequest_Success() { // 1. 准备测试数据 let testData = #"{ "someKey": "someValue", "idleTimeoutInMinutes": 5 }"#.data(using: .utf8)! let mockResult = ResultType(data: testData, response: nil, error: nil) // 替换成你真实的Result类型 mockServiceCaller.resultToReturn = mockResult let expectation = self.expectation(description: "App init request completes successfully") // 2. 调用方法 appInitService.initAppRequest { result in // 3. 验证结果 switch result { case .success(let model): XCTAssertEqual(model.idleTimeoutInMinutes, 10) // 代码里强制把这个值改成了10 expectation.fulfill() case .error(let error): XCTFail("Expected success, got error: \(error)") } } waitForExpectations(timeout: 1, handler: nil) // 验证Mock是否被正确调用 XCTAssertTrue(mockServiceCaller.didCallPerformRequest) } // 测试网络错误的场景 func testInitAppRequest_NetworkError() { // 1. 准备错误数据 let mockError = NSError(domain: "NetworkError", code: -1009, userInfo: nil) let mockResult = ResultType(data: nil, response: nil, error: mockError) mockServiceCaller.resultToReturn = mockResult let testHandledError = NSError(domain: "HandledError", code: -1009, userInfo: nil) mockErrorHandler.errorToReturn = testHandledError let expectation = self.expectation(description: "App init request returns error") // 2. 调用方法 appInitService.initAppRequest { result in // 3. 验证结果 switch result { case .error(let error): XCTAssertEqual(error as NSError, testHandledError) XCTAssertTrue(self.mockErrorHandler.didCallHandleError) expectation.fulfill() case .success(let model): XCTFail("Expected error, got success: \(model)") } } waitForExpectations(timeout: 1, handler: nil) XCTAssertTrue(mockServiceCaller.didCallPerformRequest) } }
过渡阶段的小技巧
如果你的项目里已经有大量地方使用了AppInitService.initAppRequest()这种静态调用,可以先保留一个静态方法作为过渡:
extension AppInitService { static func initAppRequest(_ completion: @escaping (_ appInitRecevingModel: Result<AppInitRecevingModel>) -> Void) { let service = AppInitService() service.initAppRequest(completion) } }
这样不用一下子修改所有调用处,慢慢替换成实例调用即可。
内容的提问来源于stack exchange,提问作者Emre Önder
相关产品推荐
相关产品推荐

