tonic-server-mock
tonic-server-mock is a utility for quickly creating mock gRPC servers based on tonic for testing and development.
The mock server and the client communicate over an in-memory duplex stream (tokio::io::duplex) instead of a real TCP socket:
- no ports to allocate — tests never conflict and can run fully in parallel,
- no networking flakiness, no firewall/CI surprises,
- the client side is a regular
tonic::transport::Channel, so generated tonic clients work unchanged.
Quick Start
Add the dependency to your Cargo.toml:
= "1.0"
Example
// Creates a mock server function.
// The first argument is the function name (e.g., mock_pokemon_server).
// Then list the services to be mocked (explicit function arguments names: auth_svc, create_pokemon_svc, pokemon_fight_svc).
// Optionally: After the last semicolon, you can specify the logging namespace (default is ::tracing, can be changed to ::log).
mock_server_fn!;
async
Macro options
mock_server_fn! accepts an optional visibility modifier and an optional logging namespace:
// default visibility is pub(crate), default logger is ::tracing
mock_server_fn!;
// public function, log via the `log` crate instead of `tracing`
mock_server_fn!;
The service argument names you list become the parameter names of the generated function. Each parameter accepts anything that implements tonic's service contract (impl Svc) — a plain XxxServer::new(...) as well as an interceptor-wrapped one (see below).
Real-world patterns
The patterns below are distilled from production test suites of a multi-service gRPC system that used this crate for its integration tests.
once() vs connect() — one client or many
EndpointMock is cheap to clone and can hand out any number of channels. Each connect() call opens a fresh in-memory connection to the same mock server:
async
This makes it easy to test multi-client scenarios (e.g. two subscribers on the same streaming endpoint) or reconnection logic — every connect() goes through the server's accept path just like a real TCP connection would.
Testing auth end-to-end with interceptors
Because the generated function accepts any tonic service, interceptor-wrapped services work out of the box. A typical challenge/response auth flow — client requests a challenge, signs it, exchanges it for a JWT, then calls a protected service — can be tested against a single mock server hosting both the auth service and the protected service:
mock_server_fn!;
async
Negative cases (wrong role, expired token, missing header) are tested the same way — the interceptor rejects the call and the client observes a real tonic::Status, exactly as in production.
Injecting the mock into the code under test
Production code usually connects via tonic::transport::Endpoint. To point it at a mock instead, abstract the connection behind a small trait and implement it for both:
// only compiled for tests / the `testing` feature
Any component written against impl GrpcTryConnect (a client wrapper, a background worker that reconnects on failure, etc.) now runs unmodified against either a real endpoint or a mock server.
Testing a gRPC proxy: chaining mock servers
A proxy service (one that terminates gRPC and forwards calls upstream) is tested by standing up two mock servers: one for the upstream, one for the proxy itself. The proxy is constructed with the upstream's EndpointMock, and the test client connects to the proxy's mock:
mock_server_fn!;
mock_server_fn!;
async
The same approach scales to longer pipelines (client → proxy → aggregator → upstream), all inside a single test process with no ports.
Reusable test fixtures
For a service mocked in many tests, wrap the setup into a fixture in a shared testing module: build the services, call the generated mock function, and return the server future together with the EndpointMock. A CancellationToken gives deterministic shutdown:
mock_server_fn!;
Tests then read as three lines of setup: let mock = EngineMock::start().await.run();, get a channel from mock.endpoint, and cancel the token at the end. Fixtures can also expose ready-made helpers such as mock.authenticated_client().await that perform the auth handshake and return a client with the token interceptor already attached.
Streaming
Server-streaming and bidirectional-streaming RPCs work over the in-memory transport with no special handling — subscriptions, long-lived packet streams and backpressure behave the same as over TCP, which makes the crate suitable for testing streaming pipelines (subscribe, push N messages, drop the server or cancel the token, assert the client observes the stream ending).
License
MIT License. See LICENSE file for details.