tonic-server-mock 1.0.1

Mock tonic-based gRPC servers that run in memory without networking
Documentation
# tonic-server-mock

`tonic-server-mock` is a utility for quickly creating mock gRPC servers based on [tonic](https://github.com/hyperium/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`:

```toml
tonic-server-mock = "1.0"
```

## Example
```rust
// 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!(mock_pokemon_server; auth_svc, create_pokemon_svc, pokemon_fight_svc; ::tracing);

#[tokio::test]
async fn test_grpc_server() {
    // ...
    let auth_service = AuthServiceServer::new(AuthServiceImpl::new(Arc::clone(&auth_protocol)));
    let create_pokemon_svc = CreatePokemonServiceServer::new(CreatePokemonServiceImpl::default());
    let pokemon_fight_svc = PokemonFightServiceServer::new(PokemonFightServiceImpl::default());
    // create server mock instance (the generated function is async)
    let (server_mock_future, endpoint_mock) =
        mock_pokemon_server(auth_service, create_pokemon_svc, pokemon_fight_svc).await;
    let cancellation_token = CancellationToken::new();
    tokio::spawn({
        let cancellation_token = cancellation_token.clone();
        cancellation_token.run_until_cancelled(server_mock_future)
    });
    // create tonic channel that can be used by generated clients
    let tokio_channel = endpoint_mock.connect().await.unwrap();
    let mut auth_client = AuthServiceClient::new(tokio_channel.clone());
    let mut create_pokemon_client = CreatePokemonServiceClient::new(tokio_channel.clone());
    let mut pokemon_fight_client = PokemonFightServiceClient::new(tokio_channel);
    // ...
}

```

## Macro options

`mock_server_fn!` accepts an optional visibility modifier and an optional logging namespace:

```rust
// default visibility is pub(crate), default logger is ::tracing
mock_server_fn!(mock_server; my_svc);

// public function, log via the `log` crate instead of `tracing`
mock_server_fn!(pub mock_server; my_svc; ::log);
```

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:

```rust
#[tokio::test]
async fn two_clients_one_server() {
    let (server_future, endpoint_mock) = mock_pokemon_server(auth_svc, fight_svc).await;
    tokio::spawn(server_future);

    // single client, consume the mock:
    let channel = endpoint_mock.clone().once().await;

    // or simulate several independent clients / reconnects:
    let first_connection = endpoint_mock.connect().await;
    let second_connection = endpoint_mock.connect().await;
}
```

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:

```rust
mock_server_fn!(mock_auth_server; auth_svc, protected_svc);

#[tokio::test]
async fn should_authorize() {
    let auth_svc = AuthServiceServer::new(AuthServiceImpl::new(Arc::clone(&auth_protocol)));
    // the protected service checks tokens via a server-side interceptor
    let protected_svc = PokemonFightServiceServer::with_interceptor(
        PokemonFightServiceImpl::default(),
        ServerAuthInterceptor::new(Arc::clone(&auth_protocol)),
    );
    let (server_future, endpoint_mock) = mock_auth_server(auth_svc, protected_svc).await;
    tokio::spawn(server_future);

    let channel = endpoint_mock.once().await;

    // 1. authenticate over the plain channel
    let mut auth_client = AuthServiceClient::new(channel.clone());
    let access_token = obtain_token(&mut auth_client).await;

    // 2. call the protected service with a client-side interceptor attaching the token
    let mut client =
        PokemonFightServiceClient::with_interceptor(channel, ClientAuthInterceptor::new(access_token));
    client.fight(FightRequest { /* ... */ }).await.expect("authorized call must succeed");
}
```

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:

```rust
pub trait GrpcTryConnect {
    fn try_connect(&self) -> impl Future<Output = Result<Channel, tonic::transport::Error>> + Send;
}

impl GrpcTryConnect for tonic::transport::Endpoint {
    fn try_connect(&self) -> impl Future<Output = Result<Channel, tonic::transport::Error>> + Send {
        tonic::transport::Endpoint::connect(self)
    }
}

// only compiled for tests / the `testing` feature
#[cfg(any(test, feature = "testing"))]
impl GrpcTryConnect for tonic_server_mock::EndpointMock {
    fn try_connect(&self) -> impl Future<Output = Result<Channel, tonic::transport::Error>> + Send {
        futures::FutureExt::map(tonic_server_mock::EndpointMock::connect(self), Ok)
    }
}
```

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:

```rust
mock_server_fn!(mock_upstream; upstream_svc);
mock_server_fn!(mock_proxy; proxy_svc);

#[tokio::test]
async fn should_forward_through_proxy() {
    // real upstream service behind mock #1
    let upstream_svc = PokemonServiceServer::new(PokemonServiceImpl::default());
    let (upstream_future, upstream_endpoint) = mock_upstream(upstream_svc).await;
    tokio::spawn(upstream_future);

    // the proxy under test dials the upstream via its EndpointMock (see the trait above)
    let proxy_svc = PokemonServiceServer::new(PokemonProxy::new(upstream_endpoint));
    let (proxy_future, proxy_endpoint) = mock_proxy(proxy_svc).await;
    tokio::spawn(proxy_future);

    // the client talks to the proxy; the whole chain is in-memory
    let mut client = PokemonServiceClient::new(proxy_endpoint.once().await);
    // ... assert that requests, streams and errors propagate through the proxy
}
```

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:

```rust
mock_server_fn!(pub mock_engine_server; auth_svc, engine_svc);

pub struct EngineMock<S> {
    pub endpoint: Arc<EndpointMock>,
    pub cancellation_token: CancellationToken,
    pub server: S,
}

impl EngineMock<impl Future<Output = ()> + Send> {
    pub async fn start() -> Self {
        let auth_svc = AuthServiceServer::new(AuthServiceImpl::default());
        let engine_svc = EngineServiceServer::new(EngineServiceImpl::default());
        let (server, endpoint) = mock_engine_server(auth_svc, engine_svc).await;
        Self {
            endpoint: Arc::new(endpoint),
            cancellation_token: CancellationToken::new(),
            server,
        }
    }

    /// Spawn the server; it stops when the token is cancelled.
    pub fn run(self) -> EngineMock<tokio::task::JoinHandle<()>> {
        let token = self.cancellation_token.clone();
        EngineMock {
            endpoint: self.endpoint,
            cancellation_token: self.cancellation_token,
            server: tokio::spawn(async move {
                token.run_until_cancelled_owned(self.server).await;
            }),
        }
    }
}
```

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.