XCTest Mork

以网络请求场景为例

样例代码:

class NetworkManager {
    func loadData(from url: URL,
                  completionHandler: @escaping (NetworkResult) -> Void) {
        let task = URLSession.shared.dataTask(with: url) { data, _, error in
            // Create either a .success or .failure case of a result enum
            let result = data.map(NetworkResult.success) ?? .failure(error)
            completionHandler(result)
        }

        task.resume()
    }
}

可以由两种方法来mock数据。

  1. 创建 使用到的类的mock子类,并重写调用路径上的相关方法,来返回mock数据
// We create a partial mock by subclassing the original class
class URLSessionDataTaskMock: URLSessionDataTask {
    private let closure: () -> Void

    init(closure: @escaping () -> Void) {
        self.closure = closure
    }

    // We override the 'resume' method and simply call our closure
    // instead of actually resuming any task.
    override func resume() {
        closure()
    }
}

class URLSessionMock: URLSession {
    typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void

    // Properties that enable us to set exactly what data or error
    // we want our mocked URLSession to return for any request.
    var data: Data?
    var error: Error?

    override func dataTask(
        with url: URL,
        completionHandler: @escaping CompletionHandler
    ) -> URLSessionDataTask {
        let data = self.data
        let error = self.error

        return URLSessionDataTaskMock {
            completionHandler(data, nil, error)
        }
    }
}
//调整一下NetworkManager的实现
class NetworkManager {
    private let session: URLSession

    // By using a default argument (in this case .shared) we can add dependency
    // injection without making our app code more complicated.
    init(session: URLSession = .shared) {
        self.session = session
    }

    func loadData(from url: URL,
                  completionHandler: @escaping (NetworkResult) -> Void) {
        let task = session.dataTask(with: url) { data, _, error in
            let result = data.map(NetworkResult.success) ?? .failure(error)
            completionHandler(result)
        }

        task.resume()
    }
}
  1. 将网络请求的方法包装为session的一个方法(剥离与DataTask的关系);然后创建一个自定义的协议,该协议实现了该方法;再让URLSession类来遵循该协议,并按需求实现该方法;最后创建一个自定义mock类病遵循该协议,在该方法里返回指定的Mock数据。
//网络请求方法收敛为一个自定义方法
class NetworkManager {
    private let session: NetworkSession

    init(session: NetworkSession = URLSession.shared) {
        self.session = session
    }

    func loadData(from url: URL,
                  completionHandler: @escaping (NetworkResult) -> Void) {
        session.loadData(from: url) { data, error in
            let result = data.map(NetworkResult.success) ?? .failure(error)
            completionHandler(result)
        }
    }
}
//创建一个自定义的协议,该协议实现了该方法
protocol NetworkSession {
    func loadData(from url: URL,
                  completionHandler: @escaping (Data?, Error?) -> Void)
}
//再让URLSession类来遵循该协议,并按需求实现该方法
extension URLSession: NetworkSession {
    func loadData(from url: URL,
                  completionHandler: @escaping (Data?, Error?) -> Void) {
        let task = dataTask(with: url) { (data, _, error) in
            completionHandler(data, error)
        }

        task.resume()
    }
}
//最后创建一个自定义mock类并遵循该协议,在该方法里返回指定的Mock数据。
class NetworkSessionMock: NetworkSession {
    var data: Data?
    var error: Error?

    func loadData(from url: URL,
                  completionHandler: @escaping (Data?, Error?) -> Void) {
        completionHandler(data, error)
    }
}