Swift中的HTTP(五) 模拟测试

822 阅读4分钟

HTTP简介

HTTP基础结构

HTTP请求体

HTTP 加载请求

HTTP 模拟测试

HTTP 链式加载器

HTTP 动态修改请求

HTTP 请求选项

HTTP 重置

HTTP 取消

HTTP 限流

HTTP 重试

HTTP 基础鉴权

HTTP 自动鉴权设置

HTTP 自动鉴权

HTTP 复合加载器

HTTP 头脑风暴

HTTP 总结

我们已经了解了单个方法如何为通过网络加载请求提供基础。

然而,网络也是开发应用程序时最大的失败点之一,尤其是在单元测试方面。 当我们编写单元测试时,我们希望测试是可重复的:无论我们执行多少次,我们应该总是得到相同的结果。 如果我们的测试涉及实时网络连接,我们无法保证这一点。 由于我们实际网络请求失败的所有原因,我们的单元测试也可能失败。

因此,我们使用模拟对象来模拟网络连接,但实际上提供了一个一致且可重复的外观,我们可以通过它提供虚假数据。

由于我们已将网络接口抽象为单个方法,因此模拟它非常简单。 这是一个始终返回 200 OK 响应的 HTTPLoading 实现:

public class MockLoader: HTTPLoading {

    public func load(request: HTTPRequest, completion: @escaping (HTTPResult) -> Void) {

        let urlResponse = HTTPURLResponse(url: request.url!, statusCode: HTTPStatus(rawValue: 200), httpVersion: "1.1", headerFields: nil)!
        let response = HTTPResponse(request: request, response: urlResponse, body: nil)
        completion(.success(response))

    } 

}

我们可以在任何需要 HTTPLoading 值的地方提供 MockLoader 的实例,发送给它的任何请求都将导致 200 OK 响应,尽管主体为 nil

当我们使用模拟网络连接编写单元测试时,我们并不是在测试网络代码本身。 通过模拟网络层,我们将网络作为变量移除,这意味着网络不是被测试的对象:单元测试检查实验的变量。

我们将使用我们在上一篇文章中删除的 StarWarsAPI 类来说明这一原则:

public class StarWarsAPI {
    private let loader: HTTPLoading

    public init(loader: HTTPLoading = URLSession.shared) {
        self.loader = loader
    }

    public func requestPeople(completion: @escaping (...) -> Void) {
        var r = HTTPRequest()
        r.host = "swapi.dev"
        r.path = "/api/people"

        loader.load(request: r) { result in
            // TODO: interpret the result
            completion(...)
        }
    }
}

该类的测试将验证其行为:我们要确保它在不同情况下的行为正确。 例如,我们要确保 requestPeople() 方法在收到 200 OK 响应或 404 Not Found 响应或 500 Internal Server Error 时行为正确。 我们使用 MockLoader 模拟这些场景。 这些测试将使我们有信心在不破坏现有功能的情况下改进 StarWarsAPI 的实现。

为了满足这些需求,我们的 MockLoader 需要:

保证传入的请求是我们在测试中期望的请求 为每个请求提供自定义响应 我个人版本的 MockLoader 大致如下所示:

public class MockLoader: HTTPLoading {
    // typealiases help make method signatures simpler
    public typealias HTTPHandler = (HTTPResult) -> Void
    public typealias MockHandler = (HTTPRequest, HTTPHandler) -> Void
    
    private var nextHandlers = Array<MockHandler>()
    
    public override func load(request: HTTPRequest, completion: @escaping HTTPHandler) {
        if nextHandlers.isEmpty == false {
            let next = nextHandlers.removeFirst()
            next(request, completion)
        } else {
            let error = HTTPError(code: .cannotConnect, request: request)
            completion(.failure(error))
        }
    }
    
    @discardableResult
    public func then(_ handler: @escaping MockHandler) -> Mock {
        nextHandlers.append(handler)
        return self
    }
}

这个 MockLoader 允许我提供如何响应连续请求的个性化实现。 例如:

func test_sequentialExecutions() {
    let mock = MockLoader()
    for i in 0 ..< 5 {
        mock.then { request, handler in
            XCTAssert(request.path, "/(i)")
            handler(.success(...))
        }
    }

    for i in 0 ..< 5 {
        var r = HTTPRequest()
        r.path = "/(i)"
        mock.load(r) { result in
            XCTAssertEqual(result.response?.statusCode, .ok)
        }
    }
}

如果我们在为 StarWarsAPI 类编写测试时使用这个 MockLoader,它可能看起来像这样(我省略了 XCTestExpectations,因为它们与本次讨论没有直接关系):

class StarWarsAPITests: XCTestCase {

    let mock = MockLoader()
    lazy var api: StarWarsAPI = { StarWarsAPI(loader: mock) }()

    func test_200_OK_WithValidBody() {
        mock.then { request, handler in
            XCTAssertEqual(request.path, "/api/people")
            handler(.success(/* 200 OK with some valid JSON */))
        }
        api.requestPeople { ...
            // assert that "StarWarsAPI" correctly decoded the response
        }
    }

    func test_200_OK_WithInvalidBody() {
        mock.then { request, handler in
            XCTAssertEqual(request.path, "/api/people")
            handler(.success(/* 200 OK but some mangled JSON */))
        }
        api.requestPeople { ... 
            // assert that "StarWarsAPI" correctly realized the response was bad JSON
        }
    }

    func test_404() {
        mock.then { request, handler in
            XCTAssertEqual(request.path, "/api/people")
            handler(.success(/* 404 Not Found */))
        }
        api.requestPeople { ... 
            // assert that "StarWarsAPI" correctly produced an error
        }
    }

    func test_DroppedConnection() {
        mock.then { request, handler in
            XCTAssertEqual(request.path, "/api/people")
            handler(.failure(/* HTTPError of some kind */))
        }
        api.requestPeople { ... 
            // assert that "StarWarsAPI" correctly produced an error
        }
    }

    ...
}

当我们编写这样的测试时,我们将 StarWarsAPI 视为一个“黑匣子”:给定特定的输入条件,它是否总是产生预期的输出结果?

我们的 HTTPLoading 抽象使得交换网络堆栈的实现成为一个简单的改变。 我们所做的只是将 MockLoader 传递给初始化程序而不是 URLSession。 这里的关键是意识到,通过使我们的 StarWarsAPI 依赖于接口 (HTTPLoading) 而不是具体化 (URLSession),我们极大地增强了它的实用性并使其更易于单独使用(和测试)。

这种对特定实现的行为定义的依赖将在我们实现框架的其余部分时很好地为我们服务。 在下一篇文章中,我们会将 HTTPLoading 更改为一个类并添加一个属性,该属性将为我们可以想象的几乎所有可能的网络行为提供基础。