Testland
Browse all skills & agents

grpc-mock

Wraps gRPC server-mocking patterns for client-side tests: Go bufconn (in-memory net.Listener via google.golang.org/grpc/test/bufconn) + mockgen-generated interface mocks, Python pytest-grpc fixtures + unittest.mock patching of stubs, JVM grpc-mock library / in-process gRPC server (InProcessServerBuilder), Node @grpc/grpc-js fake server with NewServer-on-port-0. Use when writing client-side tests that need a controllable gRPC server response (success cases, error cases per grpc-status-code-mapping-reference, timeouts, and single-response error injection) without spinning up a real backend. For multi-message streaming-sequence tests (server-streaming, bidi), use grpc-streaming-test-author instead. Distinct from grpcurl-cli (ad-hoc CLI invocation against a real server) and ghz-load (perf against a real server).

Install with skills.sh (any agent)

npx skills add testland/qa --skill grpc-mock
View source

grpc-mock

Overview

Mocking a gRPC server lets client-side tests exercise success paths, every grpc.StatusCode per grpc-status-code-mapping-reference, timeouts, and streaming sequences without a real backend.

Three approaches dominate, picked by language:

ApproachMechanism
In-process gRPC serverA real grpc.Server listens on an in-memory transport (bufconn in Go, InProcessServerBuilder in JVM). Tests exercise the full client stack.
Interface mockmockgen / gomock (Go) / Mockito (JVM) / unittest.mock (Python) replace the generated client stub with a programmable mock. Faster but skips marshalling.
Standalone mock serverRun a tool like grpcmock / dishwasher as a subprocess. Cross-language client testing.

When to use

  • Client-side tests need controllable gRPC responses (success + error matrix).
  • Streaming-RPC tests need a server that sends a deterministic sequence.
  • Mid-test failure injection (deadline / cancellation / status code).
  • Tests must not depend on a running backend (offline CI).
  • Cross-service contract testing: want the client to see what the contract says, not what the current server happens to do.

Authoring

Go: bufconn + in-process server

Per pkg.go.dev/google.golang.org/grpc/test/bufconn (opens in new window), bufconn.Listener is the canonical in-memory transport:

package myservice_test

import (
    "context"
    "net"
    "testing"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/credentials/insecure"
    "google.golang.org/grpc/status"
    "google.golang.org/grpc/test/bufconn"
    pb "example.com/proto"
)

const bufSize = 1024 * 1024

type fakeServer struct {
    pb.UnimplementedUserServiceServer
    nextResponse *pb.User
    nextErr      error
}

func (f *fakeServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
    if f.nextErr != nil {
        return nil, f.nextErr
    }
    return f.nextResponse, nil
}

func setupClient(t *testing.T, fake *fakeServer) pb.UserServiceClient {
    lis := bufconn.Listen(bufSize)
    s := grpc.NewServer()
    pb.RegisterUserServiceServer(s, fake)
    go func() { _ = s.Serve(lis) }()
    t.Cleanup(func() { s.Stop() })

    conn, err := grpc.DialContext(context.Background(), "bufnet",
        grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
            return lis.Dial()
        }),
        grpc.WithTransportCredentials(insecure.NewCredentials()),
    )
    if err != nil { t.Fatal(err) }
    t.Cleanup(func() { conn.Close() })

    return pb.NewUserServiceClient(conn)
}

func TestGetUser_NotFound(t *testing.T) {
    fake := &fakeServer{
        nextErr: status.Error(codes.NotFound, "user does not exist"),
    }
    client := setupClient(t, fake)

    _, err := client.GetUser(context.Background(), &pb.GetUserRequest{Id: "missing"})

    st, _ := status.FromError(err)
    if st.Code() != codes.NotFound {
        t.Fatalf("got %v, want NotFound", st.Code())
    }
}

Per grpc-status-code-mapping-reference: assert on status.Code(), not on error message strings.

Go: gomock / mockgen (interface mock)

For tests that don't need the marshalling/transport stack:

go install go.uber.org/mock/mockgen@latest
mockgen -source=gen/user_grpc.pb.go -destination=mocks/user_mock.go
import (
    "testing"
    "go.uber.org/mock/gomock"
    pb "example.com/proto"
    mocks "example.com/mocks"
)

func TestServiceWithMockClient(t *testing.T) {
    ctrl := gomock.NewController(t)
    mockClient := mocks.NewMockUserServiceClient(ctrl)

    mockClient.EXPECT().
        GetUser(gomock.Any(), gomock.Eq(&pb.GetUserRequest{Id: "u1"})).
        Return(&pb.User{Id: "u1", Name: "Alice"}, nil)

    // Test the code that uses mockClient ...
}

Tradeoff: doesn't exercise serialisation; faster, less fidelity.

Python: in-process server + pytest fixture

import grpc
import pytest
from concurrent import futures
from user_pb2 import User, GetUserRequest
from user_pb2_grpc import UserServiceServicer, add_UserServiceServicer_to_server, UserServiceStub


class FakeUserService(UserServiceServicer):
    next_response = None
    next_status = None

    def GetUser(self, request, context):
        if self.next_status is not None:
            context.abort(self.next_status, "fake error")
        return self.next_response


@pytest.fixture
def fake_service():
    return FakeUserService()


@pytest.fixture
def grpc_channel(fake_service):
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=1))
    add_UserServiceServicer_to_server(fake_service, server)
    port = server.add_insecure_port("[::]:0")
    server.start()
    channel = grpc.insecure_channel(f"localhost:{port}")
    yield channel
    server.stop(grace=0)


def test_get_user_not_found(fake_service, grpc_channel):
    fake_service.next_status = grpc.StatusCode.NOT_FOUND
    stub = UserServiceStub(grpc_channel)
    with pytest.raises(grpc.RpcError) as exc:
        stub.GetUser(GetUserRequest(id="missing"))
    assert exc.value.code() == grpc.StatusCode.NOT_FOUND

server.add_insecure_port("[::]:0") lets the OS pick a free port - important for parallel test execution.

Python: unittest.mock patching of stub

from unittest.mock import patch, MagicMock
import grpc

def test_service_with_mock_stub():
    with patch("myapp.user_pb2_grpc.UserServiceStub") as MockStub:
        instance = MockStub.return_value
        instance.GetUser.return_value = User(id="u1", name="Alice")

        # Test the code that uses UserServiceStub ...

JVM: InProcessServerBuilder

import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.inprocess.InProcessServerBuilder;
import io.grpc.testing.GrpcCleanupRule;

@Rule public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule();

@Test
public void getUser_notFound() throws Exception {
    String serverName = InProcessServerBuilder.generateName();
    grpcCleanup.register(InProcessServerBuilder
        .forName(serverName)
        .directExecutor()
        .addService(new UserServiceGrpc.UserServiceImplBase() {
            @Override
            public void getUser(GetUserRequest req, StreamObserver<User> obs) {
                obs.onError(Status.NOT_FOUND
                    .withDescription("user does not exist")
                    .asRuntimeException());
            }
        })
        .build()
        .start());

    UserServiceGrpc.UserServiceBlockingStub stub = UserServiceGrpc.newBlockingStub(
        grpcCleanup.register(InProcessChannelBuilder
            .forName(serverName)
            .directExecutor()
            .build()));

    StatusRuntimeException e = assertThrows(StatusRuntimeException.class,
        () -> stub.getUser(GetUserRequest.newBuilder().setId("missing").build()));
    assertEquals(Status.Code.NOT_FOUND, e.getStatus().getCode());
}

Node / TypeScript: @grpc/grpc-js + port 0

import * as grpc from "@grpc/grpc-js";
import { UserServiceService } from "./generated/user_grpc_pb";

function createServer(handlers: Partial<UserServiceServer>) {
  const server = new grpc.Server();
  server.addService(UserServiceService, handlers);
  return new Promise<{ port: number; server: grpc.Server }>((resolve, reject) => {
    server.bindAsync("127.0.0.1:0", grpc.ServerCredentials.createInsecure(), (err, port) => {
      if (err) return reject(err);
      server.start();
      resolve({ port, server });
    });
  });
}

test("GetUser returns NOT_FOUND", async () => {
  const { port, server } = await createServer({
    getUser: (_call, callback) => {
      callback({ code: grpc.status.NOT_FOUND, details: "user does not exist" });
    },
  });
  const client = new UserServiceClient(`localhost:${port}`, grpc.credentials.createInsecure());
  await expect(() => promisify(client.getUser.bind(client))({ id: "missing" }))
    .rejects.toMatchObject({ code: grpc.status.NOT_FOUND });
  server.forceShutdown();
});

Running

These tests run as ordinary unit tests:

go test ./...                 # Go
pytest tests/                 # Python
mvn test                      # JVM
npm test                      # Node

Per-language test runners; no separate harness needed.

Parsing results

Test failures point to:

  • Wrong status code - fix server-side error mapping or test expectation per grpc-status-code-mapping-reference.
  • Marshalling errors - proto definition / generated code drift; regenerate via buf-cli-lint-breaking-build.
  • Timeout - server not responding; fake handler hung; check context.WithTimeout usage.

CI integration

jobs:
  unit-tests-with-grpc-mocks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-go@v5
      - run: go test ./... -race -timeout=60s

-race is critical for mock-server tests - concurrent client + server goroutines often surface races.

Anti-patterns

Anti-patternWhy it failsFix
Asserting on error message stringsBrittle to i18n / wordingAssert on status.Code()
Hard-coded ports (8080) in testsPort conflicts in parallel CIUse bufconn (Go), [::]:0 (Python), InProcessChannel (JVM), port 0 (Node)
Sharing one mock server across testsTest order matters; flakyPer-test setup; t.Cleanup / fixture teardown
Mocking gRPC stub without server registrationTests skip codec, marshalling, error mappingIn-process server preferred over interface mock for service-level tests
Returning a Go error directly (not status.Error)Client sees Code: Unknown (per grpc-status-code-mapping-reference)Always wrap with status.Errorf(codes.X, "...")
Mocking streaming methods with one responseTests don't exercise multi-message logicUse a real stream + Send multiple times
Forgetting server.Stop() in teardownGoroutine leaks; future tests pollutet.Cleanup / pytest fixture yield
No -race flag in Go testsConcurrent races slip throughAlways go test -race in CI

Limitations

  • No wire-level fault injection in-process. For partial-byte cutoffs or middlebox-induced errors, use a real network + toxiproxy / tc.
  • bufconn / InProcessServer skip TLS. Tests that exercise TLS-specific code paths need a real server.
  • Streaming-test ergonomics differ per language. Bidi streaming with deterministic interleavings is fiddly everywhere; see grpc-streaming-test-author.
  • Mock-stub approach loses error-mapping fidelity. A real status.Errorf wraps differently than a hand-constructed Go error.
  • Doesn't replace contract tests. Mocks reflect what this test expects; they don't enforce that the real server actually produces those responses. Pair with protobuf-compat-checking (in the qa-contract-testing plugin).

References

Related skills

buf-cli-lint-breaking-build

Wraps the buf CLI for protobuf PR gating: `buf build` (compile .proto), `buf lint` (STANDARD rules: snake_case fields, Service suffix), `buf breaking --against {ref}` (detect wire/codegen breakage vs a git/BSR baseline), and `buf format`. Use as the CI proto-lint + breaking-change gate, or to debug a breaking failure by rule ID (e.g. FIELD_NO_DELETE_UNLESS_NUMBER_RESERVED) and pick the FILE/PACKAGE/WIRE_JSON/WIRE ruleset per consumer. This is the detection TOOL that enforces the rules; for the catalog of what is breaking and why use protobuf-versioning-strategy-reference, and for cross-service schema contract testing use protobuf-compat-checking - not this.

ghz-load

Wraps ghz, the gRPC load testing tool, for throughput and latency benchmarking. Covers test invocation (--proto + --call + host:port; or --protoset for compiled descriptors), load parameters (-n total requests, -c concurrency, -r RPS rate limit, -z duration), output formats (json/csv/html/influx-summary for CI consumption), the metrics reported (RPS achieved, latency p50/p95/p99, status-code distribution, errors), and CI integration patterns for regression gating. Use when benchmarking a gRPC service's throughput or detecting latency regressions in CI.

grpc-interceptor-test-author

Authors unit tests for gRPC interceptor logic: Go grpc.UnaryServerInterceptor/UnaryClientInterceptor, Java ServerInterceptor/ClientInterceptor, and grpc-js client interceptors. Covers auth (Unauthenticated on bad token), retry (backoff on Unavailable), logging/tracing (metadata extraction + propagation), error-mapping (status translation), and chained interceptor ordering - by calling the interceptor directly with a spy handler, no live backend. Use when a gRPC interceptor is written or modified. Different test surface from grpc-streaming-test-author (multi-message stream sequences) and grpc-mock (service handler logic) - use those, not this, for streams or handlers.

grpc-status-code-mapping-reference

Pure-reference catalog of gRPC standard status codes - the 17 canonical codes (OK..UNAUTHENTICATED), their numeric values, semantics, retry behaviour per AIP-194 (only UNAVAILABLE is auto-retry-safe), and the gRPC-to-HTTP status mapping used by grpc-gateway (NOT_FOUND→404, INVALID_ARGUMENT→400, PERMISSION_DENIED→403, UNAUTHENTICATED→401, RESOURCE_EXHAUSTED→429, FAILED_PRECONDITION→400 not 412, ABORTED→409, UNAVAILABLE→503, DEADLINE_EXCEEDED→504, etc.). Use when designing a gRPC service's error vocabulary, writing assertions in gRPC client tests, configuring retry policies, or mapping gRPC errors to HTTP via a gateway. Consumed by buf-cli-lint-breaking-build, ghz-load, grpcurl-cli, grpc-mock, grpc-streaming-test-author.

grpc-streaming-test-author

Workflow-driven skill that builds gRPC streaming-RPC test suites from a proto definition. Classifies each RPC by pattern (unary, server-streaming, client-streaming, bidi), then emits the required categories per pattern - ordering preservation, completion semantics (server close after stream end, client half-close), cancellation, deadline handling, partial-stream failure. Produces skeletons for Go (bufconn + Send/Recv), Python (iterators), JVM (StreamObserver), Node (call.write/end). Use when adding tests for a new streaming RPC or auditing a suite for uncovered categories. Different test surface from grpc-interceptor-test-author (interceptor layer) and grpc-mock (harness); for wire-level streaming semantics use grpc-streaming-tests, not this.

grpcurl-cli

Wraps grpcurl, the curl-equivalent CLI for gRPC. Covers descriptor sources (server reflection default, --import-path + --proto for proto files, --protoset for compiled descriptor sets), service discovery (`list`, `describe`), invoking unary RPCs (`-d '{...}'`, `-d @file.json`, `-d @` for stdin), streaming RPCs (newline-delimited JSON via stdin), TLS configuration (--cacert, --cert, --key, --insecure, --plaintext), header injection (-H 'Authorization: Bearer ...'), and exit codes. Use for ad-hoc gRPC debugging, smoke testing, scriptable PR-time gates, and CLI-based interaction with reflective gRPC services.

protobuf-versioning-strategy-reference

Pure-reference catalog of protobuf3 versioning and breaking-change rules: field-number reservation (reserve on delete; 1..536870911; 19000-19999 reserved), wire-safe vs wire-incompatible changes (add/remove safe with reservation; changing a field number always breaks), compatible type conversions (int32/uint32/int64/uint64/bool; sint32/sint64; string/bytes for UTF-8; enum/int), oneof + map constraints, and buf's four breaking categories (FILE/PACKAGE/WIRE_JSON/WIRE) with rule IDs. Use when designing a schema change or picking a buf breaking ruleset. This is the catalog of what is breaking and why, not a scanner; to detect changes in CI use buf-cli-lint-breaking-build, for the gRPC status-code vocabulary use grpc-status-code-mapping-reference, and for cross-service contract testing use protobuf-compat-checking.