日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

Swift中的HTTP模擬測試示例詳解_Swift

作者:莊周曉夢 ? 更新時間: 2023-04-08 編程語言

正文

我們已經了解了單個方法如何為通過網絡加載請求提供基礎。

然而,網絡也是開發應用程序時最大的失敗點之一,尤其是在單元測試方面。 當我們編寫單元測試時,我們希望測試是可重復的:無論我們執行多少次,我們應該總是得到相同的結果。

如果我們的測試涉及實時網絡連接,我們無法保證這一點。 由于我們實際網絡請求失敗的所有原因,我們的單元測試也可能失敗。

因此,我們使用模擬對象來模擬網絡連接,但實際上提供了一個一致且可重復的外觀,我們可以通過它提供虛假數據。

由于我們已將網絡接口抽象為單個方法,因此模擬它非常簡單。

這是一個始終返回 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 類

我們將使用我們在上一篇文章中刪除的 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 需要:

保證傳入的請求是我們在測試中期望的請求 為每個請求提供自定義響應 我個人版本的 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 更改為一個類并添加一個屬性,該屬性將為我們可以想象的幾乎所有可能的網絡行為提供基礎。

原文鏈接:https://juejin.cn/post/7196889066830266424

欄目分類
最近更新