| Crates.io | grpc-rpc-server-harness |
| lib.rs | grpc-rpc-server-harness |
| version | 0.1.1 |
| created_at | 2026-01-17 15:07:20.966146+00 |
| updated_at | 2026-01-17 15:15:21.873814+00 |
| description | gRPC RPC server harness for testing mock services |
| homepage | |
| repository | https://github.com/nathan-poncet/rust-server-harness |
| max_upload_size | |
| id | 2050581 |
| size | 83,934 |
A Rust library for creating mock gRPC servers in your integration tests. Instead of mocking your gRPC client or stubs, spin up a real gRPC server that responds exactly as you configure it.
When testing code that calls gRPC services, you need to verify that:
Traditional approaches have drawbacks:
| Approach | Problem |
|---|---|
| Mock the generated stubs | Doesn't test actual protobuf serialization or HTTP/2 layer |
| Use a shared test server | Flaky tests, shared state, requires infrastructure |
| Mock at the transport layer | Complex setup, easy to miss protocol details |
Server Harness gives you:
[dev-dependencies]
grpc-rpc-server-harness = "0.1"
tokio = { version = "1", features = ["full"] }
use grpc_rpc_server_harness::prelude::*;
use std::net::SocketAddr;
#[tokio::main]
async fn main() -> Result<(), HarnessError> {
let addr: SocketAddr = "127.0.0.1:50051".parse().unwrap();
// Spawn a task to make gRPC requests
let requests_task = tokio::spawn(async move {
// Your gRPC client code here
});
// Build and execute the scenario
let collected = ScenarioBuilder::new()
.server(Tonic::bind(addr))
.collector(DefaultCollector::new())
.service(
Service::new("my.package.UserService")
.with_method(
Method::new("GetUser")
.with_handler(Handler::from_bytes(vec![/* protobuf bytes */]))
)
)
.build()
.execute()
.await?;
requests_task.await.unwrap();
// Assert on collected requests
assert_eq!(collected.len(), 1);
assert_eq!(collected[0].service, "my.package.UserService");
assert_eq!(collected[0].method, "GetUser");
Ok(())
}
Create handlers that respond dynamically based on the request:
let method = Method::new("Echo")
.with_handler(Handler::dynamic(|ctx| {
// Echo back the request with a prefix
let mut response = vec![0xFF];
response.extend_from_slice(&ctx.message.data);
Message::new(response)
}));
Serialize protobuf messages directly:
use prost::Message as ProstMessage;
#[derive(ProstMessage)]
struct GetUserResponse {
#[prost(string, tag = "1")]
name: String,
}
let handler = Handler::from_prost(&GetUserResponse {
name: "Alice".to_string(),
});
let scenario = ScenarioBuilder::new()
.server(Tonic::bind(addr))
.collector(DefaultCollector::new())
.service(
Service::new("my.package.UserService")
.with_method(Method::new("GetUser").with_handler(Handler::from_bytes(vec![])))
.with_method(Method::new("CreateUser").with_handler(Handler::from_bytes(vec![])))
)
.service(
Service::new("my.package.OrderService")
.with_method(Method::new("GetOrder").with_handler(Handler::from_bytes(vec![])))
)
.build();
βββββββββββββββββββ ββββββββββββββββββββ
β Your Code β gRPC Request β Mock Server β
β (gRPC Client) βββββββββββββββββββββΆβ (Tonic-based) β
β β HTTP/2 + Protobuf β β
β ββββββββββββββββββββββ Returns bytes β
β β Protobuf Response β you configured β
βββββββββββββββββββ ββββββββββββββββββββ
β
βΌ
Auto-shutdown when
all handlers consumed
β
βΌ
ββββββββββββββββββββ
β Collected Requestsβ
β (service, method, β
β message bytes) β
ββββββββββββββββββββ
MIT - see LICENSE for details.