用Mockito进行Rust HTTP测试的教程

559 阅读5分钟

在为分布式系统的一部分(如微服务)编写自动化测试时,往往会遇到被测服务调用外部Web服务的问题。

在这篇文章中,我们将看看mockito库,它提供了一种在测试中模拟这种请求的方法,使编写这些测试更加方便。

该库正在积极开发中。目前,它可以在方法、路径、查询、头文件和正文上匹配请求,也可以结合这些匹配器。

实施

好了,我们先来写我们的小程序。

首先,我们简单地创建一个hyper 服务器。

type Error = Box<dyn std::error::Error + Send + Sync + 'static>;
type Result<T> = std::result::Result<T, Error>;
type HttpClient = Client<HttpsConnector<HttpConnector>>;

async fn run_server() -> Result<()> {
    let client = init_client();

    let new_service = make_service_fn(move |_| {
        let client_clone = client.clone();
        async { Ok::<_, Error>(service_fn(move |req| route(req, client_clone.clone()))) }
    });

    let addr = "127.0.0.1:3000".parse().unwrap();
    let server = Server::bind(&addr).serve(new_service);

    println!("Listening on http://{}", addr);
    let res = server.await?;
    Ok(res)
}

#[tokio::main]
async fn main() -> Result<()> {
    run_server().await?;
    Ok(())
}

init_client 函数中,我们简单地初始化一个hyper HTTPS客户端,而route 则负责路由请求。我们只提供两个端点/basic/double

async fn route(req: Request<Body>, client: HttpClient) -> Result<Response<Body>> {
    let mut response = Response::new(Body::empty());

    match (req.method(), req.uri().path()) {
        (&Method::GET, "/basic") => {
            *response.body_mut() = basic(req, &client).await?;
        }
        (&Method::GET, "/double") => {
            *response.body_mut() = double(req, &client).await?;
        }
        _ => {
            *response.status_mut() = StatusCode::NOT_FOUND;
        }
    };
    Ok(response)
}

fn init_client() -> HttpClient {
    let https = HttpsConnector::new();
    Client::builder().build::<_, Body>(https)
}

处理程序本身并不特别有趣,我们只是发出HTTP请求,并将响应解析为一个结构。一个是猫的事实,一个是Todos。

#[derive(Serialize, Deserialize)]
struct CatFact {
    text: String,
}

#[derive(Serialize, Deserialize)]
struct TODO {
    title: String,
}

async fn basic(_req: Request<Body>, client: &HttpClient) -> Result<Body> {
    let res = do_get_req(&get_todo_url(), &client).await?;
    let body = to_bytes(res.into_body()).await?;
    let todo: TODO = from_slice(&body)?;
    Ok(todo.title.into())
}

async fn double(_req: Request<Body>, client: &HttpClient) -> Result<Body> {
    let res_todo = do_get_req(&get_todo_url(), &client).await?;
    let body_todo = to_bytes(res_todo.into_body()).await?;
    let todo: TODO = from_slice(&body_todo)?;

    let res_cats = do_get_req(&get_cats_url(), &client).await?;
    let body_cats = to_bytes(res_cats.into_body()).await?;
    let fact: CatFact = from_slice(&body_cats)?;
    Ok(format!("Todo: {}, Cat Fact: {}", todo.title, fact.text).into())
}

async fn do_get_req(uri: &str, client: &HttpClient) -> Result<Response<Body>> {
    let request = Request::builder()
        .method(Method::GET)
        .uri(uri)
        .body(Body::empty())?;
    let res = client.request(request).await?;
    Ok(res)
}

在上面的片段中,有一个辅助函数用于执行GET 请求,还有两个处理程序,分别执行一个和两个连续的请求。

现在是关于mockito 的有趣部分。上面,我们使用get_cats_urlget_todo_url 函数来获取我们使用的外部服务的URL。这些函数看起来像这样。

#[cfg(test)]
use mockito;

#[cfg(not(test))]
const CATS_URL: &str = "https://cat-fact.herokuapp.com";

#[cfg(not(test))]
const TODO_URL: &str = "https://jsonplaceholder.typicode.com";

fn get_cats_url() -> String {
    #[cfg(not(test))]
    let url = format!("{}/facts/random", CATS_URL);
    #[cfg(test)]
    let url = format!("{}/facts/random", mockito::server_url());
    url
}

fn get_todo_url() -> String {
    #[cfg(not(test))]
    let url = format!("{}/todos/1", TODO_URL);
    #[cfg(test)]
    let url = format!("{}/todos/1", mockito::server_url());
    url
}

在每一种情况下,我们检查我们是否在test 上下文,如果是,我们就使用mockito 服务器提供的网址,该服务器在我们的测试中运行。否则,我们只是使用实际的URL。

好的,如果我们运行这个应用程序,我们会简单地看到服务器启动,然后,调用两个端点,我们会得到一个包含猫的事实和/或Todos的字符串响应。

现在,让我们来看看如何为这个简单的网络应用编写一个集成测试。

为了运行我们的服务器,我们需要一个运行时间,在那里我们将生成它。然后,我们将等待一小段时间,让服务器出现。

一旦这些设置完成,我们就向服务器发出请求,并对结果执行断言。

不过在这之前,我们要在mockito 注册一个mock() 。所有这些看起来就像下面这样。

#[cfg(test)]
mod tests {
    use super::*;
    use mockito::mock;
    use tokio::runtime::Runtime;

    #[test]
    fn test_basic() {
        let _mt = mock("GET", "/todos/1")
            .with_status(200)
            .with_header("content-type", "application/json")
            .with_body(r#"{"title": "get another cat"}"#)
            .create();

        let mut rt = Runtime::new().unwrap();
        let client = init_client();

        // start server
        rt.spawn(run_server());

        // wait for server to come up
        std::thread::sleep(std::time::Duration::from_millis(50));

        // make requests
        let req_fut = client.request(
            Request::builder()
                .method(Method::GET)
                .uri("http://localhost:3000/basic")
                .body(Body::empty())
                .unwrap(),
        );
        let res = rt.block_on(req_fut).unwrap();
        let body = rt.block_on(to_bytes(res.into_body())).unwrap();

        assert_eq!(std::str::from_utf8(&body).unwrap(), "get another cat");
    }

所以在开始的时候,我们模拟每一个GET 调用到url/todos/1 ,返回一个HTTP 200,并有一个硬编码的JSON响应。这意味着,每一次用模拟中的URL调用上述的mockito::server_url() ,都会得到这个响应。我们还可以在这里做更复杂的模拟,通过regex和更多的东西来匹配URL。

在这种情况下,在服务器启动后,我们使用运行时的block_on 函数来执行请求并等待结果--对于读取正文也是如此。

现在,让我们为我们的第二个端点写一个测试。

#[test]
fn test_double() {
    let mut rt = Runtime::new().unwrap();
    let client = init_client();
    let _mc = mock("GET", "/facts/random")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(r#"{"text": "cats are the best living creatures in the universe"}"#)
        .create();

    let _mt = mock("GET", "/todos/1")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(r#"{"title": "get another cat"}"#)
        .create();

    // start server
    rt.spawn(run_server());

    // wait for server to come up
    std::thread::sleep(std::time::Duration::from_millis(50));

    // make requests
    let req_fut = client.request(
        Request::builder()
            .method(Method::GET)
            .uri("http://localhost:3000/double")
            .body(Body::empty())
            .unwrap(),
    );
    let res = rt.block_on(req_fut).unwrap();
    let body = rt.block_on(to_bytes(res.into_body())).unwrap();

    assert_eq!(
        std::str::from_utf8(&body).unwrap(),
        "Todo: get another cat, Cat Fact: cats are the best living creatures in the universe"
    );
}

这个测试基本上是一样的,不同的是,我们实际上必须模拟两个URL(TODO和Cats)。如果这种集成测试对你来说很熟悉,你可能已经注意到mockito 的一个当前限制--你只能使用一个mockito::server_url

因此,如果你有两个你想模拟的不同的服务,它们有不同的基本URL,但有相同的路径,比如example.org/hellohello.org/hello ,你不能轻易为它们提供不同的模拟。

在这种情况下,你可以选择在请求中匹配其他东西,这也是该库将来可能会支持的东西。

完整的示例代码包括可以在这里找到。

结论

对于测试网络服务,或一般的应用程序,它们依赖于与几个外部API的通信,mockito 似乎是一个很好的解决方案,使编写集成测试更容易。

虽然这个库还在开发中,还缺少一些东西,但对于大多数的使用情况来说,它已经工作得非常好了,API和设置也非常直观。

资源