詳解 Rust 如何 Mock HTTP 服務

大家好,我是螃蟹哥。

在 Rust 如何模擬外部 HTTP 服務以進行自動測試和原型設計?

在某些時候,大多數開發人員需要測試與外部 HTTP 服務交互的應用程序,例如第三方 API、身份驗證提供程序或數據源。我們並非始終可以使用這些服務,尤其是在自動測試或新服務原型設計期間。爲了驗證我們的應用程序是否按預期使用這些 API,我們需要一些工具來驗證傳出的請求,並模仿針對我們的用例和測試場景量身定製的 API 響應。這就是模擬(mocking)庫可以提供幫助的地方。

HTTP 模擬庫通常允許你創建 HTTP 服務器,並將其配置爲自定義請求 / 響應方案。雖然大多數庫都提供了在自動測試中模擬 HTTP 服務器的功能,但有些庫還使你能夠運行可配置的獨立服務器應用程序,該應用程序一次模擬多個應用程序的 API。本文展示瞭如何使用這些工具來模擬 Rust 中的 HTTP 服務。

01 應用程序

假設我們正在構建一個 Rust 應用程序,它將爲我們創建一個 GitHub 存儲庫。爲了執行這些操作,我們將使用 GitHub REST API。然後,我們將編寫一些自動測試,通過模擬來自 GitHub 的 HTTP 響應來驗證正確的行爲。

讓我們開始吧

讓我們首先爲應用程序創建一個新的 cargo 包並命名爲:github_api_client

cargo new github_api_client --bin

我們還需要一些庫,因此將它們添加到依賴項列表中:

Cargo.toml:

[dependencies]
isahc = { version = "1.6", features = ["json"]}
serde_json = "1.0"
anyhow = "1.0"

客戶端

現在,讓我們添加以下代碼,這些代碼將允許我們訪問 GitHub REST API。我們將創建一個名爲 GithubClient 的結構體,該結構將包含訪問 GitHub API 所需的所有邏輯。

main.rs:

use isahc::{ReadResponseExt, Request, RequestExt};
use serde_json::{json, Value};
use anyhow::{Result,ensure};

pub struct GithubClient {
    base_urlString,
    tokenString,
}

impl GithubClient {
    pub fn new(token&str, base_url&str) -> GithubClient {
        GithubClient { base_urlbase_url.into(), tokentoken.into() }
    }

    pub fn create_repo(&self, name&str) -> Result<String> {
        let mut response = Request::post(format!("{}/user/repos", self.base_url))
            .header("Authorization", format!("token {}", self.token))
            .header("Content-Type", "application/json")
            .body(json!({ "name"name, "private"true }).to_string())?
            .send()?;

        let json_bodyValue = response.json()?;

        ensure!(response.status().as_u16() == 201, "Unexpected status code");
        ensure!(json_body["html_url"].is_string(), "Missing html_url in response");

        return Ok(json_body["html_url"].as_str().unwrap().into());
    }
}

fn main() {
    let github = GithubClient::new("<github-token>", "https://api.github.com");
    let url = github.create_repo("myRepo").expect("Cannot create repo");
    println!("Repo URL: {}", url);
}

我們的客戶端目前提供的唯一方法是 create_repo。它將存儲庫名稱作爲參數,並返回包含存儲庫 URL 作爲字符串值的  Result。爲了簡化此示例,我們使用 anyhow 進行一般錯誤處理。

問題

現在我們有了一個功能性應用程序,我們希望編寫一些測試,以確保它沒有任何明顯的問題。

一個棘手的部分是找到一個合適的模擬目標,以便我們可以測試客戶在不同情況下的行爲。在我們的例子中,HTTP 客戶端 Request::post 函數看起來是一個很好的起點,因爲它是我們的 GitHub 客戶端在向 GitHub API 服務器發送請求之前調用的最後一個函數。

不幸的是,如果沒有針對特定 HTTP 客戶端的專用模擬庫,則模擬 HTTP 客戶端不是很實用。這是因爲,在更大的應用程序中,我們需要重新實現 HTTP 客戶端 API 的很大一部分,以便能夠充分模擬請求 / 響應方案。那麼該怎麼辦呢?

解決方案

爲了方便地測試我們的 GitHub API 客戶端,我們可以使用 HTTP 模擬庫。這些庫可以幫助我們實現至少兩個測試目標:

在撰寫本文時,至少有以下 Rust 庫可以幫助我們做到這一點:

以下表格顯示了庫之間的比較情況:

FqAV6L

根據以上比較,目前提供最完整功能的包是 httpmock。出於這個原因,我們將在本文的其餘部分使用這個(並且因爲作者是 httpmock 的創建者)。儘管我們將使用 httpmock ,但你也可以在任何其他庫中找到大多數類似的功能。

02 創建 Mocks

在本節中,我們將編寫一些測試來驗證我們的 GitHub API 客戶端是否按預期工作。我們首先將 httpmock 添加到我們的依賴項中:

Cargo.toml:

[dev-dependencies]
httpmock = "0.6"

現在我們都準備好了。讓我們創建一個測試,以確保客戶端實現中的 "good path" 按預期工作:

main.rs:

// ...
#[cfg(test)]
mod tests {
    use crate::GithubClient;
    use httpmock::MockServer;
    use serde_json::json;

    #[test]
    fn create_repo_success_test() {
        // Arrange
        let server = MockServer::start();
        let mock = server.mock(|when, then| {
            when.method("POST")
                .path("/user/repos")
                .header("Authorization", "token TOKEN")
                .header("Content-Type", "application/json");
            then.status(201)
                .json_body(json!({ "html_url""http://example.com" }));
        });
        let client = GithubClient::new("TOKEN", &server.base_url());

        // Act
        let result = client.create_repo("myRepo");

        // Assert
        mock.assert();
        assert_eq!(result.is_ok(), true);
        assert_eq!(result.unwrap(), "http://example.com");
    }
}

我們按照 AAA (Arrange-Act-Assert) 模式 [1] 排列測試代碼,該模式將測試分爲三個部分:安排,行動和斷言。

安排(Arrange)

我們在測試中做的第一件事是創建一個 MockServer 實例(第 11 行),然後我們用它來創建一個  Mock 對象

  1. MockServer 應從傳入的 HTTP 請求中應期望的所有值(第 13-16 行)

  2. 如果任何傳入的 HTTP 請求與所有預期值匹配,則將發送回 HTTP 響應的規範(第 17-18 行)。

請注意我們如何使用 when 變量來定義請求期望,並使用 then 變量來指定響應值。

模擬服務器只有在收到滿足所有期望的 HTTP 請求時纔會響應。否則,它將以錯誤消息和 HTTP 狀態代碼 404 進行響應。

重要提示:觀察我們如何將客戶端中的 base URL 設置爲指向模擬服務器而不是真正的 GitHub API(第 20 行)。

行動(Act)

在這一部分中,我們執行測試中的代碼(第 23 行),即  GithubClientcreate_repo  方法。

斷言(Assert)

最後,我們使用 Mock 對象提供的 assert 方法(第 25 行)。此方法可確保模擬服務器只收到一個符合所有期望的 HTTP 請求。否則,它將無法通過測試並打印詳細的問題描述(請參閱下一節)。

03 調試

Mock 對象提供了 assert 方法,該方法確保我們的應用程序已向模擬服務器發送了符合所有期望的請求 (即 when)。否則,此方法將無法通過測試。在這種情況下,httpmock 將嘗試在其請求日誌中找到與你的模擬期望最相似的請求。然後,它將識別兩者之間的差異,因此你可以輕鬆發現不正確或缺失的值。

爲了演示此功能,我們將修改客戶端以在 content-type 頭部發送 text/plain,而不是 application/json。如果我們然後重新運行測試,將看到檢測到此更改,並且測試現在失敗,並顯示以下消息:

At least one request has been received, but none exactly matched the mock specification.
Here is a comparison with the most similar request (request number 1): 
1 : Expected header with name 'Content-Type' and value 'application/json' to be present in the request but it wasn't.
------------------------------------------------------------------------------------------
Expected:                [key=equals, value=equals]   Content-Type=application/json
Actual (closest match):                               content-type=text/plain

根據你的 IDE,還可以在差異查看器中看到預期值和實際值之間的差異。例如,這是它在 IntelliJ 或 CLion 中的樣子:

Differences Viewer in IntelliJ and/or CLion.

04 獨立模擬服務器

到目前爲止,我們只研究了在集成測試中模擬外部 HTTP 服務,這些服務通常在一個應用程序的上下文中運行。另一方面,端到端測試可能涉及多個應用程序。在這種情況下,多個應用程序可能需要訪問外部 HTTP 服務,而其中一些應用程序可能不容易用於測試。特別是在早期開發階段或原型設計期間,某些服務可能仍在開發中,尚未準備好使用。

當多個應用程序需要訪問模擬 API 時,在單獨的進程中運行專用的模擬服務器,以便爲多個應用程序提供模擬 API,這是可行的。這允許測試執行者配置模擬服務器,使其在每個測試場景中都有不同的行爲。

Usage of a standalone mock server in end-to-end tests.

一些模擬庫附帶一個可選的獨立模擬服務器。其他的可能有第三方擴展或社區項目,使用獨立的模擬服務器擴展庫。

例如,httpmock 附帶一個單獨的 Docker 映像 [2],允許你運行專用的模擬服務器。我們可以通過以下方式使用它:

或者,兩種模式混合使用。有關獨立模式的更多信息,請參閱 httpmock 文檔 [6]。

05 總結

本文介紹瞭如何使用模擬庫來模擬 HTTP 服務。我們已經看到了它如何允許我們驗證我們的應用程序在自動測試期間發送符合我們期望的 HTTP 請求。我們還可以模擬來自 GitHub API 的 HTTP 響應,以確保我們的應用可以相應地處理它們。此外,還展示瞭如何在開發過程中使用模擬工具來替換不可用的 HTTP 服務,並使其可以同時訪問許多應用程序。

多功能模擬工具在開發生命週期的多個階段都切實可行,而不僅僅是集成測試。但是,它們對於強化基於 HTTP 的 API 客戶端特別有用,並允許我們測試難以重現的邊緣情況。

原文鏈接:https://alexliesenfeld.com/mocking-http-services-in-rust

參考資料

[1] AAA (Arrange-Act-Assert) 模式: https://medium.com/@pjbgf/title-testing-code-ocd-and-the-aaa-pattern-df453975ab80

[2] Docker 映像: https://hub.docker.com/r/alexliesenfeld/httpmock

[3] MockServer::connect: https://docs.rs/httpmock/0.6.4/httpmock/struct.MockServer.html#method.connect

[4] MockServer::start: https://docs.rs/httpmock/0.6.4/httpmock/struct.MockServer.html#method.start

[5] 此處的示例: https://github.com/alexliesenfeld/httpmock/blob/master/tests/resources/static_yaml_mock.yaml

[6] httpmock 文檔: https://docs.rs/httpmock/0.6.4/httpmock/index.html#standalone-mode

歡迎關注「Rust 編程指北」

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/knvdbWgpIvWnOv2tXrE-Mg