在为分布式系统的一部分(如微服务)编写自动化测试时,往往会遇到被测服务调用外部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_url 和get_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/hello 和hello.org/hello ,你不能轻易为它们提供不同的模拟。
在这种情况下,你可以选择在请求中匹配其他东西,这也是该库将来可能会支持的东西。
完整的示例代码包括可以在这里找到。
结论
对于测试网络服务,或一般的应用程序,它们依赖于与几个外部API的通信,mockito 似乎是一个很好的解决方案,使编写集成测试更容易。
虽然这个库还在开发中,还缺少一些东西,但对于大多数的使用情况来说,它已经工作得非常好了,API和设置也非常直观。