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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 06:45:54