Reputation: 810
Hi I have a simple application that has one gRPC method it works as expected however I do not know how to correctly integration test it. (I'm new to rust too). i.e. I would like to call gRPC add_merchant method and check if the response contains correct values.
I have following structure:
app
proto
merchant.proto
src
main.rs
merchant.rs
tests
merchant_test.rs
build.rs
Cargo.toml
merchant.proto
syntax = "proto3";
package merchant;
service MerchantService {
rpc AddMerchant (Merchant) returns (Merchant);
}
message Merchant {
string name = 1;
}
merchant.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(())
}
main.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))
}
}
How should merchant_test.rs look like?
Upvotes: 5
Views: 3956
Reputation: 939
I almost spent 6 hours trying to do the same with tonic and I came up with the idea of using unix domain sockets to unit test the implementation of the gRPC methods. The tonic-example repo has a uds folder that can help us.
TBH I didn't find the solution by myself, it was a team effort. Here's how you can test tonic gRPC services using futures (details below).
In Cargo.toml
add
[dev-dependencies]
tokio-stream = { version = "0.1.8", features = ["net"] }
tower = { version = "0.4" }
tempfile = "3.3.0"
Then the content of tests.rs
is
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 => (),
}
}
Why using Unix sockets? It's easier to run tests in parallel and avoid port collisions on several TCP servers. We tested with TCP and it works as well, with the caveat that you need to randomize the endpoint port.
The server_and_client_stub
function starts the gRPC server listening on a UDS mounted in a temporary file. It also builds the client with the same socket connection address.
This function can be reused in all the tests.
Then in the actual unit test for the gRPC method, we start the client, receive the response and do assertions. Finally we wait for either of the futures (server/client) to complete.
I think this is one of the many possible solutions, but it works fine for our use case, hope it will helpful for you too.
Upvotes: 12