如何让系统单例更易测试

发表于:2020-10-13 09:25

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:BigNerdCoding    来源:掘金

  以 UIApplication、UIScreen  为代表的单例模式是 iOS 中最为常见的设计模式了,你可以在代码中的任意位置调用其属性或者方法。但是这种便利也给程序代码来一些负面影响,这种全局共享状态的做法对于代码测试来说简直就是噩梦。虽然我们可以对部分单例进行重构,但是系统单例依旧需要一些技巧进行改造才能变成测试友好对象。
  下面是一个使用 URLSession.share 单例的常见网络任务代码:
enum ServiceResult {
    case success(Data)
    case error(Error)
}

class DataLoaderService {

    func load(from url: URL, completionHandler: @escaping (Result) -> Void) {
        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            if let error = error {
                return completionHandler(.error(error))
            }

            completionHandler(.success(data ?? Data()))
        }

        task.resume()
    }
    
}
  URLSession.shared 单例让我们在测试时不得不面对等待和超时的情况,随着使用的地方增多代码的测试性更是会直线下降。
  使用协议对接口进行抽象
  为了让代码对测试更为友好,我们可以使用 Mock 方式将上诉代码中的单例替换掉。而要实现该目标,第一步我们就需要将上诉函数中的功能抽象为协议中的接口。
protocol NetworkEngine {
    typealias Handler = (Data?, URLResponse?, Error?) -> Void

    func performRequest(for url: URL, completionHandler: @escaping Handler)
}
  接口声明好之后接下来就是让 URLSession 实现该协议:
extension URLSession: NetworkEngine {
    typealias Handler = NetworkEngine.Handler

    func performRequest(for url: URL, completionHandler: @escaping Handler) {
        let task = dataTask(with: url, completionHandler: completionHandler)
        task.resume()
    }
}
  这样后面我们 Mock 的时候只需要关注 NetworkEngine 就好。
  将系统单例作为默认参数注入
  为了保证原有 load 中的处理不变,接下来我们需要将 URLSession.shared 单例作为依赖注入 DataLoaderService 。
class DataLoaderService {
    private let engine: NetworkEngine

    init(engine: NetworkEngine = URLSession.share) {
         self.engine = engine
    }

    func load(from url: URL, completionHandler: @escaping (Result) -> Void) {
       engine.performRequest(for: url) { (data, response, error) in
            if let error = error {
                return completionHandler(.error(error))
            }

            completionHandler(.success(data ?? Data()))
        }
    }

}
  这里通过默认参数的方式,我们将原有的 URLSession.share 单例作为依赖注入到 DataLoaderService 中保证了原有功能的不变。
  在测试中进行 Mock
  在上面的改造完成后,我们就可以在进行单元测试时使用 Mock 的方式解决单例在测试中的问题。
func testLoadingData() {
    class NetworkEngineMock: NetworkEngine {
        typealias Handler = NetworkEngine.Handler 

        var requestedURL: URL?

        func performRequest(for url: URL, completionHandler: @escaping Handler) {
            requestedURL = url

            let data = “Hello world”.data(using: .utf8)
            completionHandler(data, nil, nil)
        }
    }

    let engine = NetworkEngineMock()
    let loader = DataLoaderService(engine: engine)

    var result: ServiceResult?
    let url = URL(string: “mock/api”)!
    loader.load(from: url) { result = $0 }

    XCTAssertEqual(engine.requestedURL, url)
    XCTAssertEqual(result, .data(“Hello world”.data(using: .utf8)!))
}
  在上诉代码中我实现了一个 NetworkEngine 协议的简单 Mock 类,并且将其作为依赖性注入到 DataLoaderService 实例中。在 Mock 类 NetworkEngineMock 我们并没有真正的请求网络接口而是直接返回硬编码,这样进一步减少了测试时的复杂度。
  总结
  通过上面三个简单的步骤,我们就能完成让原有的方法变得更易测试。当然该方法并不囿于系统单例的使用场景的改造,这种面向协议的接口服务设计在 Swift 中有更大的舞台。

      本文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号