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. Also carries the interceptor-layer test patterns (Go / Java / grpc-js auth, retry, logging, error-mapping, chained ordering via a spy handler) in references/interceptors.md. Use when writing client-side tests that need a controllable gRPC server response (success cases, error cases, timeouts, single-response error injection) without spinning up a real backend, or when testing a gRPC interceptor. 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-mockgrpc-mock
Overview
Mocking a gRPC server lets client-side tests exercise success paths, every grpc.StatusCode (per the status-code catalog in grpc-streaming-test-author, references/status-codes.md), timeouts, and streaming sequences without a real backend. The in-process harness is also the substrate for interceptor tests - see references/interceptors.md.
Three approaches dominate, picked by language:
| Approach | Mechanism |
|---|---|
| In-process gRPC server | A real grpc.Server listens on an in-memory transport (bufconn in Go, InProcessServerBuilder in JVM). Tests exercise the full client stack. |
| Interface mock | mockgen / gomock (Go) / Mockito (JVM) / unittest.mock (Python) replace the generated client stub with a programmable mock. Faster but skips marshalling. |
| Standalone mock server | Run a tool like grpcmock / dishwasher as a subprocess. Cross-language client testing. |
When to use
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 the status-code catalog in grpc-streaming-test-author (references/status-codes.md): 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.goimport (
"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_FOUNDserver.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 # NodePer-language test runners; no separate harness needed.
Parsing results
Test failures point to:
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-pattern | Why it fails | Fix |
|---|---|---|
| Asserting on error message strings | Brittle to i18n / wording | Assert on status.Code() |
Hard-coded ports (8080) in tests | Port conflicts in parallel CI | Use bufconn (Go), [::]:0 (Python), InProcessChannel (JVM), port 0 (Node) |
| Sharing one mock server across tests | Test order matters; flaky | Per-test setup; t.Cleanup / fixture teardown |
| Mocking gRPC stub without server registration | Tests skip codec, marshalling, error mapping | In-process server preferred over interface mock for service-level tests |
Returning a Go error directly (not status.Error) | Client sees Code: Unknown | Always wrap with status.Errorf(codes.X, "...") |
| Mocking streaming methods with one response | Tests don't exercise multi-message logic | Use a real stream + Send multiple times |
Forgetting server.Stop() in teardown | Goroutine leaks; future tests pollute | t.Cleanup / pytest fixture yield |
No -race flag in Go tests | Concurrent races slip through | Always go test -race in CI |
Limitations
References
Go interceptor tests
View source (opens in new window)Go interceptor tests
All Go examples construct the interceptor function directly and invoke it with a spy handler (the next leg). No live backend. Always assert on status.Code(), never on error message strings (message text is not part of the gRPC contract).
Auth interceptor - rejects bad token
Type signatures per pkg.go.dev/google.golang.org/grpc#UnaryServerInterceptor (opens in new window):
type UnaryServerInterceptor func(
ctx context.Context,
req any,
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (any, error)Test pattern: pass a ctx with missing/bad authorization metadata and verify the interceptor returns codes.Unauthenticated (code 16 per pkg.go.dev/google.golang.org/grpc/codes (opens in new window)) without calling the handler.
package auth_test
import (
"context"
"testing"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
// authInterceptor returns Unauthenticated when "authorization" header is absent.
func authInterceptor(
ctx context.Context,
req any,
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (any, error) {
md, ok := metadata.FromIncomingContext(ctx)
// metadata.FromIncomingContext docs: all keys are lowercase.
if !ok || len(md.Get("authorization")) == 0 {
return nil, status.Error(codes.Unauthenticated, "missing authorization header")
}
return handler(ctx, req)
}
func TestAuthInterceptor_MissingToken_ReturnsUnauthenticated(t *testing.T) {
handlerCalled := false
spy := func(ctx context.Context, req any) (any, error) {
handlerCalled = true
return "ok", nil
}
ctx := context.Background() // no metadata attached
_, err := authInterceptor(ctx, nil, nil, spy)
if handlerCalled {
t.Fatal("handler must not be called when token is absent")
}
st, _ := status.FromError(err)
if st.Code() != codes.Unauthenticated {
t.Fatalf("got %v, want Unauthenticated", st.Code())
}
}
func TestAuthInterceptor_ValidToken_CallsHandler(t *testing.T) {
handlerCalled := false
spy := func(ctx context.Context, req any) (any, error) {
handlerCalled = true
return "ok", nil
}
md := metadata.Pairs("authorization", "Bearer valid-token")
ctx := metadata.NewIncomingContext(context.Background(), md)
_, err := authInterceptor(ctx, nil, nil, spy)
if err != nil {
t.Fatal(err)
}
if !handlerCalled {
t.Fatal("handler must be called for valid token")
}
}Retry interceptor - exponential backoff on Unavailable
codes.Unavailable (code 14) is the canonical "transient, retry" signal per pkg.go.dev/google.golang.org/grpc/codes (opens in new window). A retry interceptor wraps a grpc.UnaryClientInterceptor:
type UnaryClientInterceptor func(
ctx context.Context,
method string,
req, reply any,
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
opts ...grpc.CallOption,
) errorTest: count how many times invoker is called and confirm backoff delays using a fake clock.
func TestRetryInterceptor_RetriesOnUnavailable(t *testing.T) {
callCount := 0
invoker := func(ctx context.Context, method string, req, reply any,
cc *grpc.ClientConn, opts ...grpc.CallOption) error {
callCount++
if callCount < 3 {
return status.Error(codes.Unavailable, "overloaded")
}
return nil
}
interceptor := retryInterceptor(maxRetries(3), noSleep()) // inject fake sleep
err := interceptor(context.Background(), "/svc/Method", nil, nil, nil, invoker)
if err != nil {
t.Fatalf("expected success after retries, got %v", err)
}
if callCount != 3 {
t.Fatalf("expected 3 invocations, got %d", callCount)
}
}
func TestRetryInterceptor_DoesNotRetryPermissionDenied(t *testing.T) {
callCount := 0
invoker := func(_ context.Context, _ string, _, _ any,
_ *grpc.ClientConn, _ ...grpc.CallOption) error {
callCount++
return status.Error(codes.PermissionDenied, "denied")
}
interceptor := retryInterceptor(maxRetries(3), noSleep())
err := interceptor(context.Background(), "/svc/Method", nil, nil, nil, invoker)
st, _ := status.FromError(err)
if st.Code() != codes.PermissionDenied {
t.Fatalf("got %v, want PermissionDenied", st.Code())
}
if callCount != 1 {
t.Fatalf("must not retry on PermissionDenied, got %d calls", callCount)
}
}The noSleep() option injects a no-op sleep function to keep tests fast. Never use time.Sleep inside interceptor tests.
Logging/tracing - metadata propagation
Per pkg.go.dev/google.golang.org/grpc/metadata#FromIncomingContext (opens in new window), metadata keys are always lowercase. A logging interceptor reads x-trace-id and x-request-id from incoming metadata and attaches them to the logger context.
func TestLoggingInterceptor_PropagatesTraceID(t *testing.T) {
var capturedTraceID string
spy := func(ctx context.Context, req any) (any, error) {
// The interceptor must enrich ctx with trace ID before calling handler.
capturedTraceID = traceIDFromContext(ctx) // your helper
return "ok", nil
}
md := metadata.Pairs("x-trace-id", "trace-abc-123")
ctx := metadata.NewIncomingContext(context.Background(), md)
_, err := loggingInterceptor(ctx, nil, nil, spy)
if err != nil {
t.Fatal(err)
}
if capturedTraceID != "trace-abc-123" {
t.Fatalf("trace ID not propagated: got %q", capturedTraceID)
}
}For client-side propagation use metadata.AppendToOutgoingContext per pkg.go.dev/google.golang.org/grpc/metadata#AppendToOutgoingContext (opens in new window):
ctx = metadata.AppendToOutgoingContext(ctx, "x-trace-id", traceID)Chained interceptor ordering
Per pkg.go.dev/google.golang.org/grpc#ChainUnaryInterceptor (opens in new window), the first interceptor passed to grpc.ChainUnaryInterceptor is the outermost (called first). Test ordering explicitly when auth must run before logging:
func TestChainOrder_AuthBeforeLogging(t *testing.T) {
var callOrder []string
authInt := func(ctx context.Context, req any, info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (any, error) {
callOrder = append(callOrder, "auth")
return handler(ctx, req)
}
logInt := func(ctx context.Context, req any, info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (any, error) {
callOrder = append(callOrder, "log")
return handler(ctx, req)
}
// Build a chain and invoke it with a no-op handler.
chained := chainUnary(authInt, logInt) // your thin wrapper around ChainUnaryInterceptor
_, _ = chained(context.Background(), nil, nil,
func(ctx context.Context, req any) (any, error) { return nil, nil })
if callOrder[0] != "auth" || callOrder[1] != "log" {
t.Fatalf("wrong order: %v", callOrder)
}
}Streaming server interceptor
grpc.StreamServerInterceptor signature per pkg.go.dev/google.golang.org/grpc#StreamServerInterceptor (opens in new window):
type StreamServerInterceptor func(
srv any,
ss grpc.ServerStream,
info *grpc.StreamServerInfo,
handler grpc.StreamHandler,
) errorTest using a fake grpc.ServerStream that captures the metadata header sent before the first message:
type fakeStream struct {
grpc.ServerStream
ctx context.Context
headers metadata.MD
}
func (f *fakeStream) Context() context.Context { return f.ctx }
func (f *fakeStream) SendHeader(md metadata.MD) error {
f.headers = md
return nil
}
func TestStreamAuthInterceptor_MissingToken(t *testing.T) {
fs := &fakeStream{ctx: context.Background()} // no metadata
err := streamAuthInterceptor(nil, fs, nil, func(srv any, stream grpc.ServerStream) error {
t.Fatal("handler must not be called")
return nil
})
st, _ := status.FromError(err)
if st.Code() != codes.Unauthenticated {
t.Fatalf("got %v, want Unauthenticated", st.Code())
}
}grpc-js client interceptor tests
View source (opens in new window)grpc-js client interceptor tests
@grpc/grpc-js exposes client interceptors as a channel option. The package README confirms "Client Interceptors" as a supported feature at github.com/grpc/grpc-node/tree/master/packages/grpc-js (opens in new window). An interceptor is a function (options, nextCall) => InterceptingCall.
Client interceptor - auth-header injection
Test an auth-header injector by building an InterceptingCall with a RequesterBuilder that captures the outbound metadata:
import * as grpc from "@grpc/grpc-js";
import { InterceptingCall, InterceptorOptions, NextCall } from "@grpc/grpc-js";
function authInterceptor(token: string) {
return (options: InterceptorOptions, nextCall: NextCall): InterceptingCall => {
return new InterceptingCall(nextCall(options), {
start(metadata, listener, next) {
metadata.add("authorization", `Bearer ${token}`);
next(metadata, listener);
},
});
};
}
// Test using a spy on the nextCall layer
test("authInterceptor injects Authorization header", () => {
let capturedMetadata: grpc.Metadata | undefined;
const fakeNext: NextCall = (_options) =>
new InterceptingCall(null as any, {
start(metadata, _listener, _next) {
capturedMetadata = metadata;
},
});
const interceptorFn = authInterceptor("my-token");
const call = interceptorFn({} as InterceptorOptions, fakeNext);
call.start(new grpc.Metadata(), {} as grpc.Listener);
expect(capturedMetadata?.get("authorization")).toEqual(["Bearer my-token"]);
});Register on a channel:
const client = new UserServiceClient(address, credentials, {
interceptors: [authInterceptor("my-token")],
});gRPC interceptor test authoring
View source (opens in new window)gRPC interceptor test authoring
gRPC interceptors apply cross-cutting behavior (auth, retry, logging, error mapping) to every RPC without touching service logic. They are one of the primary sources of subtle gRPC bugs: silent metadata drops, wrong ordering in a chain, and retry storms that ignore backoff. This skill produces isolated unit tests for each interceptor behavior.
Per the gRPC interceptors guide (opens in new window), interceptors are "per-call" and are split into client-side and server-side variants, each further divided into unary and streaming forms.
Scope: the host SKILL.md authors tests for service handler logic using an in-process server; this reference tests the interceptor layer itself, not the handler. grpc-streaming-test-author covers multi-message stream sequences; this reference covers interceptors that wrap streams (e.g., a server stream interceptor that injects a header before the first message).
Interceptor taxonomy
| Variant | Go type (pkg.go.dev/google.golang.org/grpc) | Java type (grpc-java javadoc) | grpc-js |
|---|---|---|---|
| Server unary | grpc.UnaryServerInterceptor | ServerInterceptor.interceptCall | N/A (server-only via grpc package) |
| Server streaming | grpc.StreamServerInterceptor | ServerInterceptor.interceptCall | N/A |
| Client unary | grpc.UnaryClientInterceptor | ClientInterceptor.interceptCall | InterceptorProvider option |
| Client streaming | grpc.StreamClientInterceptor | ClientInterceptor.interceptCall | InterceptorProvider option |
Full type signatures are at the reference links in each language playbook.
Authoring strategy: call the interceptor directly
The canonical test pattern for all languages is:
This avoids spinning up a full in-process server just to test cross-cutting logic. Use the host SKILL.md's in-process server only when testing the interaction between an interceptor and a handler.
Language playbooks
Each playbook holds the full type signatures, test patterns, and runnable examples for one language surface:
How to use
Worked example
Scenario: a Go client retry interceptor must retry transient failures but never retry an auth failure.
Full code for both tests is in go-interceptors.md (opens in new window).
Running
These tests run as ordinary unit tests in each language:
go test ./... -run TestAuth -race # Go: -race catches metadata races
mvn test -Dtest=AuthInterceptorTest # Java / Maven
npx jest --testPathPattern=interceptor # Node / JestUse -race in Go: concurrent ctx + metadata access in interceptors surfaces races that pass without the flag.
CI integration
jobs:
interceptor-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v5
with: { go-version: stable }
- run: go test ./... -race -count=1 -timeout=30s-count=1 disables the test cache so metadata-mutation tests are not silently skipped on re-runs.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Asserting on error message strings | Text is not part of the gRPC contract; changes with i18n | Assert on status.Code() only |
| Testing the interceptor only via an end-to-end call | Chain bugs and ordering issues are invisible when everything succeeds | Call the interceptor function directly with a spy handler |
Assuming intercept() and interceptForward() are identical | Java ServerInterceptors.intercept() applies interceptors in reverse order per the javadoc | Use interceptForward() when declaration order must match execution order |
time.Sleep inside retry-interceptor tests | Slow tests; sleep duration is arbitrary | Inject a fake sleep function via an option or dependency parameter |
| Sharing a single metadata map across test cases | Map mutation bleeds between cases | Construct a fresh metadata.MD / Metadata per test |
| Not testing the "does not retry" case for non-transient codes | Retry interceptors that retry PermissionDenied cause auth-storm bugs | Add explicit tests for codes.PermissionDenied and codes.InvalidArgument |
| Embedding real tokens in test metadata | Secrets in source history | Use constant placeholder strings like "Bearer test-token-value" |
Limitations
References
Java interceptor tests
View source (opens in new window)Java interceptor tests
Java interceptors are objects; call interceptCall directly with stubbed ServerCall / Channel collaborators (Mockito) and assert on captured Status and Metadata.
ServerInterceptor - auth rejection
ServerInterceptor.interceptCall signature per grpc-java javadoc (opens in new window):
<ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next)Test with a ServerCall stub that captures the close() call:
import io.grpc.*;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
public class AuthInterceptorTest {
private final ServerInterceptor interceptor = new AuthInterceptor();
@SuppressWarnings("unchecked")
@Test
public void missingAuthHeader_closesWithUnauthenticated() {
ServerCall<Object, Object> call = mock(ServerCall.class);
Metadata headers = new Metadata(); // no authorization key
ServerCallHandler<Object, Object> next = mock(ServerCallHandler.class);
interceptor.interceptCall(call, headers, next);
verify(call).close(
argThat(s -> s.getCode() == Status.Code.UNAUTHENTICATED),
any(Metadata.class));
verifyNoInteractions(next);
}
}Registration per grpc-java javadoc ServerInterceptors.intercept (opens in new window)
// Last-listed interceptor fires first:
ServerServiceDefinition def =
ServerInterceptors.intercept(serviceImpl, authInterceptor, loggingInterceptor);
// First-listed interceptor fires first:
ServerServiceDefinition def =
ServerInterceptors.interceptForward(serviceImpl, authInterceptor, loggingInterceptor);ClientInterceptor - outbound token injection
ClientInterceptor.interceptCall signature per grpc-java javadoc (opens in new window):
<ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method,
CallOptions callOptions,
Channel next)Test that the interceptor attaches the authorization key to outbound headers by capturing Metadata passed to ClientCall.start():
@Test
public void tokenInjector_attachesAuthorizationHeader() {
ClientInterceptor interceptor = new TokenInjectorInterceptor("Bearer tok");
Channel channel = mock(Channel.class);
ClientCall<Object, Object> innerCall = mock(ClientCall.class);
when(channel.newCall(any(), any())).thenReturn(innerCall);
ClientCall<Object, Object> call =
interceptor.interceptCall(methodDescriptor(), CallOptions.DEFAULT, channel);
Metadata headers = new Metadata();
call.start(mock(ClientCall.Listener.class), headers);
String auth = headers.get(Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER));
assertEquals("Bearer tok", auth);
}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 and carries the catalog of what is breaking and why (field-number reservation, wire-safe vs wire-incompatible changes, oneof/map constraints, the four buf categories) in references/versioning-strategy.md; 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-streaming-test-author
The single gRPC-streaming test home: builds 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); carries the 17-code gRPC status catalog (retry semantics per AIP-194, grpc-gateway HTTP mapping) in references/status-codes.md and the wire-level / live-server streaming patterns (deadline propagation, server-side cancellation, metadata, ghz load) in references/wire-level-testing.md. Use when adding tests for a new or existing streaming RPC, auditing a suite for uncovered categories, or asserting status-code behavior. Different test surface from grpc-mock (the in-process harness itself).
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.