CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/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.

74

Quality

93%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

SKILL.md

name:
grpc-interceptor-test-author
description:
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.
metadata:
{"keywords":"grpc, interceptor, unary, streaming, auth, retry, metadata, tracing, go, java, grpc-js"}

grpc-interceptor-test-author

Overview

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, interceptors are "per-call" and are split into client-side and server-side variants, each further divided into unary and streaming forms.

Differentiation from sibling skills:

  • grpc-mock authors tests for service handler logic using an in-process server. This skill tests the interceptor layer itself, not the handler.
  • grpc-streaming-test-author covers multi-message stream sequences. This skill covers interceptors that wrap streams (e.g., a server stream interceptor that injects a header before the first message).

Interceptor taxonomy

VariantGo type (pkg.go.dev/google.golang.org/grpc)Java type (grpc-java javadoc)grpc-js
Server unarygrpc.UnaryServerInterceptorServerInterceptor.interceptCallN/A (server-only via grpc package)
Server streaminggrpc.StreamServerInterceptorServerInterceptor.interceptCallN/A
Client unarygrpc.UnaryClientInterceptorClientInterceptor.interceptCallInterceptorProvider option
Client streaminggrpc.StreamClientInterceptorClientInterceptor.interceptCallInterceptorProvider 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:

  1. Construct the interceptor function/object directly.
  2. Invoke it with a crafted context/metadata and a spy or stub handler (the next leg in the chain).
  3. Assert on: what the handler received, what status code was returned, and what metadata was set.

This avoids spinning up a full in-process server just to test cross-cutting logic. Use the in-process server from grpc-mock 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:

  • Go - server/client unary, retry with fake clock, logging/tracing metadata propagation, chained ordering, streaming server interceptor: references/go-interceptors.md.
  • Java - ServerInterceptor auth rejection, ClientInterceptor outbound token injection, intercept() vs interceptForward() ordering: references/java-interceptors.md.
  • grpc-js - client interceptor auth-header injection via InterceptingCall: references/grpc-js-interceptors.md.

How to use

  1. Identify the interceptor under test and its variant (server/client, unary/streaming) using the taxonomy table.
  2. Open the matching language playbook (Go, Java, or grpc-js).
  3. Construct the interceptor directly and craft the input context/metadata for the behavior under test (auth, retry, logging, error-mapping, ordering).
  4. Wire a spy/stub handler (the next leg) that records what it received and whether it was called at all.
  5. Invoke the interceptor and assert on status code, handler invocation count, and set/propagated metadata - never on error message strings.
  6. Add the negative case (e.g., a non-transient code must not be retried) and a fresh metadata map per test to prevent bleed.
  7. Run with the language's isolation flags (-race, -count=1) and wire into CI.

Worked example

Scenario: a Go client retry interceptor must retry transient failures but never retry an auth failure.

  1. Under test: retryInterceptor(maxRetries(3), noSleep()), a grpc.UnaryClientInterceptor.
  2. Craft a spy invoker that returns codes.Unavailable on the first two calls and nil on the third, incrementing callCount each time.
  3. Invoke the interceptor with context.Background() and the spy; assert err == nil and callCount == 3 - it retried twice, then succeeded.
  4. Add the negative case: a spy invoker that always returns codes.PermissionDenied; assert st.Code() == codes.PermissionDenied and callCount == 1 - a non-transient code is surfaced immediately, not retried.
  5. Run go test ./... -run TestRetry -race -count=1. Result: two passing tests proving backoff fires on Unavailable and is skipped on PermissionDenied, with no auth-storm regression.

Full code for both tests is in references/go-interceptors.md.

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 / Jest

Use -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-patternWhy it failsFix
Asserting on error message stringsText is not part of the gRPC contract; changes with i18nAssert on status.Code() only
Testing the interceptor only via an end-to-end callChain bugs and ordering issues are invisible when everything succeedsCall the interceptor function directly with a spy handler
Assuming intercept() and interceptForward() are identicalJava ServerInterceptors.intercept() applies interceptors in reverse order per the javadocUse interceptForward() when declaration order must match execution order
time.Sleep inside retry-interceptor testsSlow tests; sleep duration is arbitraryInject a fake sleep function via an option or dependency parameter
Sharing a single metadata map across test casesMap mutation bleeds between casesConstruct a fresh metadata.MD / Metadata per test
Not testing the "does not retry" case for non-transient codesRetry interceptors that retry PermissionDenied cause auth-storm bugsAdd explicit tests for codes.PermissionDenied and codes.InvalidArgument
Embedding real tokens in test metadataSecrets in source historyUse constant placeholder strings like "Bearer test-token-value"

Limitations

  • Does not cover wire-level fault injection. For testing that an interceptor survives partial bytes or TCP resets, use a real network with toxiproxy.
  • Streaming interceptors need fake ServerStream / ClientStream implementations. Minimal fakes satisfy most tests; complex multi-message sequences belong in grpc-streaming-test-author.
  • grpc-js server interceptors are not in scope. The @grpc/grpc-js server does not expose a ServerInterceptor extension point in the same way the Java or Go servers do.
  • grpc.ChainUnaryInterceptor ordering only applies to the server. Client chaining uses grpc.WithChainUnaryInterceptor; the two have the same semantics but different registration functions per pkg.go.dev/google.golang.org/grpc.

References

SKILL.md

tile.json