如何集成测试 tonic 应用程序

use*_*282 5 rust rust-tonic

您好,我有一个简单的应用程序,它有一种 gRPC 方法,它按预期工作,但我不知道如何正确地集成测试它。(我也是生锈新手)。即我想调用 gRPC add_merchant 方法并检查响应是否包含正确的值。

我有以下结构:

app
  proto
    merchant.proto
  src
    main.rs
    merchant.rs
  tests
    merchant_test.rs
  build.rs
  Cargo.toml
Run Code Online (Sandbox Code Playgroud)

商家.proto

syntax = "proto3";
package merchant;

service MerchantService {
  rpc AddMerchant (Merchant) returns (Merchant);
}

message Merchant {
  string name = 1;
}
Run Code Online (Sandbox Code Playgroud)

商人.rs

mod merchant;

use merchant::merchant_service_server::MerchantServiceServer;
use merchant::MerchantServiceImpl;
use tonic::transport::Server;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "127.0.0.1:50051".parse()?;
    let merchant = MerchantServiceImpl::default();

    Server::builder()
        .add_service(MerchantServiceServer::new(merchant))
        .serve(addr)
        .await?;

    Ok(())
}
Run Code Online (Sandbox Code Playgroud)

主程序.rs

use tonic::{Request, Response, Status};
use crate::merchant::merchant_service_server::MerchantService;

tonic::include_proto!("merchant");

#[derive(Debug, Default)]
pub struct MerchantServiceImpl {
}

#[tonic::async_trait]
impl MerchantService for MerchantServiceImpl {
    async fn add_merchant(&self, request: Request<Merchant>) -> 
Result<Response<Merchant>, Status> {
       let response = Merchant {
           name: "name".to_string()
       };

       Ok(Response::new(response))
   }
}
Run Code Online (Sandbox Code Playgroud)

merchant_test.rs 应该是什么样子?

Fra*_*zzi 10

我几乎花了 6 个小时尝试用 tonic 做同样的事情,并提出了使用 unix 域套接字来对 gRPC 方法的实现进行单元测试的想法。tonic-example 仓库有一个uds文件夹可以帮助我们。

老实说,我自己没有找到解决方案,这是团队的努力。以下是如何使用 future 测试 tonic gRPC 服务(详细信息如下)。

Cargo.toml添加

[dev-dependencies]
tokio-stream = { version = "0.1.8", features = ["net"] }
tower = { version = "0.4" }
tempfile = "3.3.0"
Run Code Online (Sandbox Code Playgroud)

那么内容tests.rs就是

use std::future::Future;
use std::sync::Arc;
use tempfile::NamedTempFile;
use tokio::net::{UnixListener, UnixStream};
use tokio_stream::wrappers::UnixListenerStream;
use tonic::transport::{Channel, Endpoint, Server, Uri};
use tonic::{Request, Response, Status};
use tower::service_fn;

struct ServerStub {}

#[tonic::async_trait]
impl MerchantService for ServerStub {
    async fn add_merchant(&self, _request: Request<Merchant>)) -> Result<Response<Merchant>, Status> {
        // Stub your response
        return Ok(Response::new(Merchant {
           name: "name".to_string()
       };));
    }
}

async fn server_and_client_stub() -> (impl Future<Output = ()>, MerchantServiceClient<Channel>) {
    let socket = NamedTempFile::new().unwrap();
    let socket = Arc::new(socket.into_temp_path());
    std::fs::remove_file(&*socket).unwrap();

    let uds = UnixListener::bind(&*socket).unwrap();
    let stream = UnixListenerStream::new(uds);

    let serve_future = async {
        let result = Server::builder()
            .add_service(MerchantServiceServer::new(ServerStub {}))
            .serve_with_incoming(stream)
            .await;
        // Server must be running fine...
        assert!(result.is_ok());
    };

    let socket = Arc::clone(&socket);
    // Connect to the server over a Unix socket
    // The URL will be ignored.
    let channel = Endpoint::try_from("http://any.url")
        .unwrap()
        .connect_with_connector(service_fn(move |_: Uri| {
            let socket = Arc::clone(&socket);
            async move { UnixStream::connect(&*socket).await }
        }))
        .await
        .unwrap();

    let client = MerchantServiceClient::new(channel);

    (serve_future, client)
}

// The actual test is here
#[tokio::test]
async fn add_merchant_test() {
    let (serve_future, mut client) = server_and_client_stub().await;

    let request_future = async {
        let response = client
            .add_merchant(Request::new(Merchant{
               // Stub your request here 
             }))
            .await
            .unwrap()
            .into_inner();
        // Validate server response with assertions
        assert_eq!(response.name,"name".to_string());
    };

    // Wait for completion, when the client request future completes
    tokio::select! {
        _ = serve_future => panic!("server returned first"),
        _ = request_future => (),
    }
}
Run Code Online (Sandbox Code Playgroud)

为什么使用 Unix 套接字?并行运行测试并避免多个 TCP 服务器上的端口冲突更加容易。我们使用 TCP 进行了测试,它也能正常工作,但需要注意的是您需要随机化端点端口。

server_and_client_stub函数启动 gRPC 服务器侦听安装在临时文件中的 UDS。它还使用相同的套接字连接地址构建客户端。该函数可以在所有测试中重复使用。

然后在 gRPC 方法的实际单元测试中,我们启动客户端,接收响应并进行断言。最后,我们等待其中一个 future(服务器/客户端)完成。

我认为这是众多可能的解决方案之一,但它对于我们的用例来说效果很好,希望它对您也有帮助。