---
title: "gRPC Mocks, Tests and Contract Checks From a .proto | Speedscale"
description: "Turn a .proto file into gRPC mocks and tests with proxymock, record real responses, and catch field type changes that protobuf decoding silently accepts."
canonical: "https://speedscale.com/blog/test-grpc-service-from-proto/"
---

![gRPC Mocks, Tests and Contract Checks From a .proto](https://speedscale.com/_astro/featured-image.BxlgUwJq_ZX0VJI.webp)

# gRPC Mocks, Tests and Contract Checks From a .proto

Oct 26, 2026

[By Matt LeRay](https://speedscale.com/blog/author/matt-leray/)

[API testing](https://speedscale.com/blog/category/api-testing/) [Developer tools](https://speedscale.com/blog/category/developer-tools/) [Microservices](https://speedscale.com/blog/category/microservices/)

* * *

Change one field in a `.proto` from `double` to `string`, and nothing complains. The code compiles, the deploy goes green, and a service still built from the old contract keeps answering with a `double`. Here’s what Go’s protobuf runtime does when a client on the new contract reads that answer:

```
msg := dynamicpb.NewMessage(fraudCheckResponse) // risk_score is now a string
err := proto.Unmarshal(recordedBytes, msg)      // the server still sends a double
fmt.Println(err, msg.Get(riskScore).String(), len(msg.GetUnknown()))
```

```
<nil>  9
```

No error. `risk_score` comes back empty, and the nine bytes of the double are tucked away as unknown fields. Protobuf is designed to tolerate a field whose wire type doesn’t match, so the break surfaces later as a zero value in someone else’s service.

This post walks through catching that before it ships, starting from nothing but the `.proto` and [proxymock](https://speedscale.com/proxymock/). You will generate gRPC mocks and tests from the contract and serve the mock to `grpcurl`. Then you will record the real service’s responses with the generated tests, see what the recording covers, and flag the type change in CI. Everything below ran on a real service, and every output is copied from that run.

```
flowchart LR
    P[fraud.proto] --> G[proxymock generate]
    G --> M[Mock for callers]
    G --> T[Inbound tests]
    T --> R[proxymock record]
    S[Real service] --> R
    R --> C[proxymock coverage]
    R --> V[proxymock validate in CI]
```

## What you need

*   proxymock 2.5.1249 or newer ([install](https://docs.speedscale.com/proxymock/getting-started/installation/)). It runs locally and needs no account.
*   [grpcurl](https://github.com/fullstorydev/grpcurl) and Go, to build the example service.
*   A gRPC service and its `.proto`. The example is the fraud-check service from Speedscale’s open source [microsvc](https://github.com/speedscale/microsvc) demo, one unary RPC that scores a transaction:

```
service FraudChecker {
  rpc CheckTransaction(TransactionRequest) returns (FraudCheckResponse);
}

message TransactionRequest {
  string account_id = 1;
  string user_id = 2;
  double amount = 3;
  string transaction_type = 4;
  string merchant_category = 5;
}

message FraudCheckResponse {
  bool approved = 1;
  double risk_score = 2;
  string reason = 3;
}
```

Clone it and build the service:

```
git clone https://github.com/speedscale/microsvc && cd microsvc
(cd backend/fraud-service && go build -o ../../fraud-service ./cmd/server)
```

## Step 1: Generate mocks and tests from the .proto

`proxymock generate` reads the `.proto` and writes one call per unary RPC. Outbound calls are mocks that stand in for the service. Inbound calls are tests that drive requests at it:

```
proxymock generate --direction outbound --out ./proxymock/mocks-fraud ./proto/fraud/fraud.proto
proxymock generate --direction inbound --out ./proxymock/tests-fraud ./proto/fraud/fraud.proto
```

```
Processing 1 RPCs from protobuf specification...
Processing RPC: /fraud.v1.FraudChecker/CheckTransaction
  Generated: grpc_fraud.v1.FraudChecker_CheckTransaction_OK_inbound.md (status OK)
...
PLACEHOLDERS: 1 of 1 inbound test(s) still send generated placeholders ("example_value" or ${{param:...}}).
```

Each file is a gRPC call over HTTP/2, in the same shape proxymock writes when it records real traffic, so the mock server and replay use it as is. The messages are synthesized from the field types: strings are `example_value`, numbers sit mid-range, repeated fields get two items, an enum gets its first non-zero value, and fields declared `optional` stay empty unless you pass `--include-optional`. Streaming RPCs are skipped and listed in the summary.

If your `.proto` imports other files, pass their root with `-I`, just as you would to `protoc`. To generate part of a large contract, filter by method with `--include-paths '/fraud.v1.FraudChecker/*'`.

## Step 2: Serve the mock to a gRPC client

Start the mock server on a mapped port. The backend address is deliberately dead, and `--no-passthrough` keeps anything that misses from going anywhere real:

```
proxymock mock --in ./proxymock/mocks-fraud --map 15051=localhost:15099 --no-passthrough
```

Call it like the real service:

```
grpcurl -plaintext -proto proto/fraud/fraud.proto \
  -d '{"account_id":"example_value","user_id":"example_value","amount":50.5,"transaction_type":"example_value","merchant_category":"example_value"}' \
  localhost:15051 fraud.v1.FraudChecker/CheckTransaction
```

```
{
  "approved": true,
  "riskScore": 50.5,
  "reason": "example_value"
}
```

That is a working gRPC dependency with no service behind it. One thing to know: a gRPC mock matches the whole request message, so a call with different values misses. With `--no-passthrough`, a gRPC client reports the miss as `Unimplemented`. The mock’s output shows the closest mock and the fields that differ, with your request on the `<` line:

```
< http:requestBodyJSON: {"1:LEN":{"12:I64":3544385894514713443},"2:LEN":"user-7","3:I64":4623226492472524800,"4:LEN":"DEPOSIT","5:LEN":"5411"}
> http:requestBodyJSON: {"1:LEN":"example_value","2:LEN":"example_value","3:I64":4632304060471443456,"4:LEN":"example_value","5:LEN":"example_value"}
```

The odd-looking field 1 is the account id `acct-1001`. Step 3 explains why it reads that way.

To make it answer your client’s real requests, edit the request values in the mock file, or better, record the real service once and mock from that recording, as [How to Mock gRPC APIs](https://speedscale.com/blog/mock-grpc-apis/) shows.

## Step 3: Record the real responses

A generated test is a driver, not a baseline. Its expected response was made up, and its request sends `example_value`, which a real service may reject. Open the inbound test and replace the placeholders. The body shows fields by number, so match each one to the `.proto` (`account_id = 1`, `user_id = 2`, and so on):

```
{
  "1:LEN": "acct-1001",
  "2:LEN": "user-7",
  "3:I64": 4632304060471443456,
  "4:LEN": "DEPOSIT",
  "5:LEN": "5411"
}
```

`I64` is a `double` shown as its raw 64 bits, here 50.5, so leave it unless the value matters. Then run the service under the recorder and replay the edited test through the recorder’s inbound port:

```
proxymock record --app-port 18551 --proxy-in-port 14143 --out ./proxymock/recorded-fraud \
  -- env GRPC_PORT=18551 METRICS_PORT=18552 ./fraud-service
```

```
proxymock replay --in ./proxymock/tests-fraud --test-against http://localhost:14143 \
  --out ./proxymock/results/realize-fraud
```

```
│ ENDPOINT                                │ METHOD │ COUNT │ AVG LATENCY │ ... │ FAILED │ STATUS CODE MISMATCH │
│ /fraud.v1.FraudChecker/CheckTransaction │ POST   │     1 │       17 ms │ ... │      0 │                   0% │
```

```
sequenceDiagram
    participant Test as Generated test
    participant Rec as proxymock record (14143)
    participant Svc as fraud-service (18551)
    Test->>Rec: CheckTransaction (edited request)
    Rec->>Svc: forwards the call
    Svc-->>Rec: real FraudCheckResponse
    Rec-->>Test: response, also written to recorded-fraud
```

The replay reports a mismatch, and that’s expected: its expected response was the synthesized one. Stop the recorder with Ctrl-C. `proxymock/recorded-fraud` now holds the request and the service’s real answer, `approved: true` with a risk score of 0.02525 and reason `OK`. That recording is what you replay as a regression test from now on.

One detail from this recording surprised me. The account id shows up like this:

```
"1:LEN": {
  "12:I64": 3544385894514713443
}
```

proxymock decodes messages without the `.proto`, so for every length-delimited field it has to guess: string, bytes or nested message. The nine bytes of `acct-1001` happen to also parse as a message holding one 64-bit field. The guess only affects the display. The bytes are written back exactly, which is what the replay sends.

## Step 4: See what the recording covers

```
proxymock coverage --spec ./proto/fraud/fraud.proto --in ./proxymock/recorded-fraud
```

```
Recording/schema coverage: 1 inbound HTTP pair(s), 1 matched
Covered: 1/1 operations, 1/1 statuses, 3/3 fields
POST /fraud.v1.FraudChecker/CheckTransaction: 1 pair(s) [localhost/2026-10-26_16-45-46.515954Z.md]
  status OK: 1 pair(s) [localhost/2026-10-26_16-45-46.515954Z.md]
    $.approved: 1 pair(s) [localhost/2026-10-26_16-45-46.515954Z.md]
    $.reason: 1 pair(s) [localhost/2026-10-26_16-45-46.515954Z.md]
    $.risk_score: 1 pair(s) [localhost/2026-10-26_16-45-46.515954Z.md]
```

Every RPC in the `.proto` is an operation, statuses are gRPC status names, and fields are response message fields. On a real service the useful part is the ranked gap list at the end: RPCs no call reached, then error statuses never seen, then fields no response ever set. Add `--json` and keep the file next to the recording to track coverage over time.

## Step 5: Catch the contract break

Back to the opening problem. Someone changes `risk_score` to a `string` in the `.proto`, but the service is still built from the old contract. Make that change in a copy of the contract, then point `validate` at it and the recording:

```
sed 's/double risk_score = 2;/string risk_score = 2;/' proto/fraud/fraud.proto > fraud-v2.proto
proxymock validate --spec ./fraud-v2.proto --in ./proxymock/recorded-fraud
```

Against the unchanged `proto/fraud/fraud.proto` the same recording is `CONFORMANT` and exits 0. Against the changed one:

```
VIOLATION  POST /fraud.v1.FraudChecker/CheckTransaction (0)  [proxymock/recorded-fraud/localhost/2026-10-26_16-45-46.515954Z.md]
  $.risk_score: type mismatch, expected string, got fixed64

checked 1 pair(s): 0 conformant, 1 violating, 0 without a spec route
```

The command exits 2. Where the protobuf runtime returned no error and an empty field, `validate` names the field and both types. It can do that because it walks the wire format itself instead of unmarshalling, so a wire type that cannot be the declared type is a finding rather than an unknown field. It also flags enum numbers the enum does not declare, field numbers the message does not declare, and missing proto2 `required` fields.

The exit codes are made for CI: 0 conformant, 2 violations, 3 a call whose method is not in the `.proto`, and 1 when the check cannot run. Run it on a recording to check that a dependency still honors its contract, or on a replay’s output to check that your change did not break your own.

## What does not work yet

*   **Streaming RPCs.** Unary RPCs work end to end. A streaming call is recorded as one call with its messages joined, and replay sends every call as unary. [Considerations for Testing gRPC Streams](https://speedscale.com/blog/considerations-for-testing-grpc-streams/) covers why streams are harder to test. So `generate` skips streaming RPCs, and you should keep them out of a regression baseline for now.
*   **Generated mocks answer one request.** As in step 2, a generated mock matches the exact request it was generated with. Record the real traffic, or edit the request, before relying on it for a client that sends real values.

## From an AI coding assistant

The same flow runs from an AI coding assistant through the proxymock MCP server. Its `generate` tool takes the `.proto` path and import roots. `proxymock inventory` finds the `.proto` files in a repository and suggests this exact path when a service has no recorded traffic.

## Next steps

*   The [proxymock gRPC docs](https://docs.speedscale.com/proxymock/grpc/) cover recording, mocking, generating and validating in more depth.
*   Run `proxymock generate --help` to see every flag.
*   Point it at your own `.proto`, and run `validate` in CI on the next contract change.

## Table of Contents

## Stop writing API mocks by hand

proxymock records real traffic from your running app and replays it as mocks for HTTP, gRPC, Postgres, Kafka, and more. Install locally, activate once, and start recording.

[Install proxymock](https://docs.speedscale.com/proxymock/getting-started/installation/) [View the docs](https://docs.speedscale.com/proxymock/)
