详解 Rust 如何 Mock HTTP 服务

polarisxu

共 7455字,需浏览 15分钟

 · 2022-03-01

在 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

我们还需要一些库,因此将它们添加到依赖项列表中:

  • isahc作为我们的 HTTP 客户端库
  • serde_json便于 JSON 序列化和反序列化
  • custom_error 用来创建自定义错误类型

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_url: String,
    token: String,
}

impl GithubClient {
    pub fn new(token: &str, base_url: &str) -> GithubClient {
        GithubClient { base_url: base_url.into(), token: token.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_body: Value = 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("""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 模拟库。这些库可以帮助我们实现至少两个测试目标:

  • 验证客户端发送的所有 HTTP 请求是否正确(即包含所有预期值)。
  • 模拟来自 GitHub API 的 HTTP 响应,看看我们的客户是否可以相应地处理它们。

在撰写本文时,至少有以下 Rust 库可以帮助我们做到这一点:

  • mockito
  • httpmock
  • httptest
  • wiremock.

以下表格显示了库之间的比较情况:

LibraryExecutionCustom MatchersMockable APIsSync APIAsync APIStand-alone Mode
mockitoserialno1yesnono
httpmockparallelyesyesyesyes
httptestparallelyesyesnono
wiremockparallelyesnoyesno

根据以上比较,目前提供最完整功能的包是 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],允许你运行专用的模拟服务器。我们可以通过以下方式使用它:

  • 使用 Rust 的动态配置:端到端测试执行器是一个 Rust 测试,其使用方式与之前的 httpmock 集成测试中介绍的方式几乎相同。唯一的区别是它连接到远程模拟服务器(例如,通过使用MockServer::connect[3])而不是创建本地模拟服务器实例(例如,通过使用 MockServer::start[4])。在这种情况下,连接到服务器的所有测试函数都按顺序自动执行以避免冲突。
  • 静态 YAML 文件配置:此模式允许你使用不需要更改的静态配置设置模拟服务器。此方法不需要任何测试执行或 Rust 程序。相反,模拟服务器从 YAML 模拟定义文件中读取其配置,这些文件在结构上与 Rust API 非常相似(请参阅此处的示例[5])。

或者,两种模式混合使用。有关独立模式的更多信息,请参阅 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




往期推荐


我是 polarisxu,北大硕士毕业,曾在 360 等知名互联网公司工作,10多年技术研发与架构经验!2012 年接触 Go 语言并创建了 Go 语言中文网!著有《Go语言编程之旅》、开源图书《Go语言标准库》等。


坚持输出技术(包括 Go、Rust 等技术)、职场心得和创业感悟!欢迎关注「polarisxu」一起成长!也欢迎加我微信好友交流:gopherstudio

浏览 18
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报