Testland
Browse all skills & agents

grpc-streaming-tests

Test gRPC streaming RPCs - Server-streaming (server returns sequence), Client-streaming (client sends sequence), Bidirectional (both sides stream independently). Cover deadline + cancellation + flow control + status codes (CANCELLED, DEADLINE_EXCEEDED) + metadata. Use ghz for load, grpcurl for ad-hoc, language-native test stubs for unit/integration. Use when a service exposes server-, client-, or bidirectional-streaming RPCs and deadline, cancellation, or partial-stream status-code behavior is unverified.

Install with skills.sh (any agent)

npx skills add testland/qa --skill grpc-streaming-tests
View source

grpc-streaming-tests

Streaming RPCs need test coverage for deadline behavior, cancellation propagation, flow control under backpressure, and status-code semantics that differ from unary calls - covering all four patterns (Unary, Server-streaming, Client-streaming, Bidirectional-streaming) per the gRPC core concepts docs (opens in new window).

When to use

  • Service exposes streaming RPCs (price ticker, log tail, IoT telemetry, AI streaming inference).
  • Pre-deploy gate: deadline + cancellation propagate correctly, partial-stream errors return correct status codes.
  • Load test gate: streams handle backpressure without OOM or silent drops.

How to use

  1. Pick the test tool for the job (Step 1): native stubs for unit/integration, grpcurl for smoke, ghz for load.
  2. Prove the plumbing with a unary sanity call before touching streams (Step 2).
  3. Cover each streaming shape the service exposes - server-, client-, and bidirectional-streaming (Steps 3-5).
  4. Assert deadline propagation and client-initiated cancellation are observed server-side (Steps 6-7).
  5. Check error paths return the exact status code, not just "an error", and that request metadata round-trips (references/status-codes-metadata-load.md).
  6. Run a ghz load pass to confirm streams handle backpressure without OOM or silent drops (same reference).
  7. Gate the suite on the anti-patterns table before merge.

Step 1 - Pick the test tool

ToolStrength
Language-native stubs (Go grpc.WithBlock(), Python grpc.aio, Java ManagedChannel)Unit/integration tests
grpcurlAd-hoc + smoke tests + scripts
ghzLoad testing + benchmarks (concurrency, RPS)
mockgrpc / mockery (Go), grpc-mock (Node)Mock server stubs in unit tests

Step 2 - Unary RPC sanity (baseline)

Per the gRPC core concepts docs (opens in new window), Unary = "single request, single response." Use this to verify the RPC plumbing before testing streams:

import grpc
from orders_pb2 import OrderRequest
from orders_pb2_grpc import OrdersStub

def test_unary_create_order():
    with grpc.insecure_channel("localhost:50051") as ch:
        stub = OrdersStub(ch)
        resp = stub.CreateOrder(OrderRequest(item_count=2), timeout=5.0)
        assert resp.order_id != ""

Step 3 - Server-streaming test

Per the gRPC core concepts docs (opens in new window), server-streaming = "client sends a request and gets a stream to read a sequence of messages back."

def test_server_streaming_price_ticker():
    with grpc.insecure_channel("localhost:50051") as ch:
        stub = PricesStub(ch)
        stream = stub.SubscribePrices(SubscribeRequest(symbol="AAPL"), timeout=10.0)
        ticks = []
        for tick in stream:
            ticks.append(tick)
            if len(ticks) >= 5:
                stream.cancel()
                break

        assert len(ticks) == 5
        assert all(t.symbol == "AAPL" for t in ticks)

Step 4 - Client-streaming test

Per the gRPC core concepts docs (opens in new window), client-streaming = "client writes a sequence of messages and sends them to the server."

def test_client_streaming_upload():
    def chunks():
        for i in range(10):
            yield UploadChunk(seq=i, data=b"x" * 1024)

    with grpc.insecure_channel("localhost:50051") as ch:
        stub = UploadsStub(ch)
        resp = stub.Upload(chunks(), timeout=10.0)

        assert resp.total_chunks == 10
        assert resp.total_bytes == 10 * 1024

Step 5 - Bidirectional streaming + ordering

Per the gRPC core concepts docs (opens in new window), bidirectional streams "operate independently" - server may emit messages before reading any client message, after, or interleaved.

import asyncio

async def test_bidi_chat():
    async def client_messages():
        for msg in ["hello", "how are you", "bye"]:
            yield ChatMessage(text=msg)
            await asyncio.sleep(0.1)

    async with grpc.aio.insecure_channel("localhost:50051") as ch:
        stub = ChatStub(ch)
        responses = []
        async for resp in stub.Chat(client_messages()):
            responses.append(resp)

        assert len(responses) >= 3

Step 6 - Deadline propagation

Per the gRPC core concepts docs (opens in new window), "Clients specify maximum wait time; RPCs terminate with DEADLINE_EXCEEDED if exceeded."

def test_deadline_returns_correct_status():
    with grpc.insecure_channel("localhost:50051") as ch:
        stub = SlowStub(ch)
        with pytest.raises(grpc.RpcError) as exc_info:
            stub.SlowOperation(SlowRequest(), timeout=0.5)

        assert exc_info.value.code() == grpc.StatusCode.DEADLINE_EXCEEDED

Verify the server-side:

def test_server_observes_deadline_propagation():
    # Service should respect deadline and cancel its own downstream calls
    with grpc.insecure_channel("localhost:50051") as ch:
        stub = OrchestratorStub(ch)
        with pytest.raises(grpc.RpcError):
            stub.Compose(ComposeRequest(), timeout=0.1)

    # Verify downstream call observed the cancellation
    downstream_state = fetch_downstream_state()
    assert downstream_state.cancelled_count >= 1

Step 7 - Cancellation behavior

Per the gRPC core concepts docs (opens in new window), "Either party can terminate an RPC immediately. Changes made before a cancellation are not rolled back."

def test_cancellation_is_observed_server_side():
    with grpc.insecure_channel("localhost:50051") as ch:
        stub = LongRunningStub(ch)
        future = stub.LongOperation.future(LongRequest())
        time.sleep(0.5)
        future.cancel()

        # Server should record cancellation
        time.sleep(0.5)
        state = fetch_server_metrics()
        assert state.cancelled_count >= 1

Status codes, metadata, and load testing

See references/status-codes-metadata-load.md for the full status-code matrix (OK, CANCELLED, DEADLINE_EXCEEDED, INVALID_ARGUMENT, UNAVAILABLE, ...), a request/response metadata round-trip test, and load testing with ghz.

Worked example

A prices service exposes SubscribePrices, a server-streaming RPC. QA needs to confirm the client receives ordered ticks and that cancelling the stream is observed server-side.

  1. Start with the unary sanity call (Step 2) to confirm the channel and stubs are wired.
  2. Open the stream with a 10s deadline and read 5 ticks: stream = stub.SubscribePrices(SubscribeRequest(symbol="AAPL"), timeout=10.0).
  3. After the 5th tick, call stream.cancel() and break (Step 3).
  4. Assert len(ticks) == 5 and every t.symbol == "AAPL".
  5. Add a cancellation check (Step 7): fetch server metrics and assert cancelled_count >= 1, proving the server observed the client cancel rather than orphaning work.

Result: the ticker stream is verified for ordered delivery, a clean 10s deadline, and server-side cancellation - the three behaviors a server-streaming RPC most often regresses on.

Anti-patterns

Anti-patternWhy it failsFix
Skip deadline + cancellation testsProduction cancellation orphans server-side workSteps 6 + 7
Test only OK and INTERNAL pathsStatus-code regressions go silentlyTest the matrix (status-codes reference)
Use BatchSpanProcessor or similar buffering on test clientStreams "complete" before all messages flushAlways synchronous in tests
Tests share a single channel across goroutinesChannel state contamination flakesPer-test channel
Generate proto stubs at test runtimeCI flakes on plugin churnGenerate in build phase + commit

Limitations

  • gRPC-Web uses HTTP/1.1 fallback; some streaming patterns (client/bidi) are not supported. Test gRPC-Web specifically if used.
  • Long-lived bidi streams hide individual-message error codes; channel-level state matters more.
  • ghz protobuf reflection requires the server to enable reflection service (not always on in production builds).

References

  • gRPC core concepts docs (opens in new window) - RPC patterns, deadlines, cancellation, status codes, metadata
  • websocket-tests - WebSocket alternative for non-gRPC stacks
  • server-sent-events-tests - one-way HTTP streaming alternative

gRPC status codes, metadata, and load testing

View source (opens in new window)

gRPC status codes, metadata, and load testing

Cross-cutting call semantics beyond the four streaming patterns: the full status-code matrix, request/response metadata round-trip, and load testing with ghz.

Status codes

CodeWhen
OKSuccess
CANCELLEDClient cancelled
DEADLINE_EXCEEDEDDeadline elapsed
INVALID_ARGUMENTClient error in request
UNAUTHENTICATEDNo / bad credentials
PERMISSION_DENIEDAuthenticated but not authorized
RESOURCE_EXHAUSTEDQuota / rate limit
INTERNALServer bug
UNAVAILABLEServer transient unreachable (clients should retry)

Test the error path returns the right code, not just "an error":

def test_invalid_argument_returns_correct_code():
    with grpc.insecure_channel("localhost:50051") as ch:
        stub = OrdersStub(ch)
        with pytest.raises(grpc.RpcError) as exc:
            stub.CreateOrder(OrderRequest(item_count=-1))
        assert exc.value.code() == grpc.StatusCode.INVALID_ARGUMENT

Metadata

Per the gRPC core concepts docs (opens in new window), metadata is "key-value pairs" case-insensitive ASCII keys; binary values use -bin suffix.

def test_request_metadata_round_trip():
    with grpc.insecure_channel("localhost:50051") as ch:
        stub = OrdersStub(ch)
        metadata = (("x-trace-id", "abc123"),)
        resp, call = stub.CreateOrder.with_call(OrderRequest(), metadata=metadata)

        # Server reflects request-id in response trailing metadata
        trailing = call.trailing_metadata()
        assert ("x-trace-id-echo", "abc123") in trailing

Load test with ghz

ghz \
  --insecure \
  --proto orders.proto \
  --call orders.Orders/CreateOrder \
  -d '{"item_count":1}' \
  -c 50 \
  -n 10000 \
  localhost:50051

Reports RPS, p50/p95/p99 latency. For streaming RPCs use --stream-call-count flag (consult ghz docs).

Related skills

mqtt-tests

Test MQTT v5.0 with Mosquitto broker in CI + paho-mqtt clients - QoS 0 / 1 / 2 delivery semantics, retained messages, Last Will and Testament (LWT), shared subscriptions ($share/group/topic), $SYS topic introspection. Critical for IoT, embedded, and M2M systems where wire-level guarantees matter. Use when a product speaks MQTT on the wire and QoS 1 / 2 redelivery, retained-message state, or LWT behavior needs a broker-backed test - including smoke-testing a new broker auth / ACL / persistence config.

server-sent-events-tests

Test Server-Sent Events (SSE) flows, one-way server-to-client push only (not bidirectional, use websocket-tests for client-to-server messaging): `EventSource` API on the browser side (`onmessage`, `onerror`, `readyState` 0/1/2), event stream format (`data:`, `event:`, `id:`, `retry:`), `Last-Event-ID` reconnect-with-replay header, content-type `text/event-stream`, and HTTP/1.1 connection-pool limits. Use Playwright for browser-side, raw HTTP client for server-side stream tests. Use when a feature pushes updates over `text/event-stream` and the reconnect interval, `Last-Event-ID` replay, or per-origin connection ceiling has no coverage.

sse-load-tests

Load-tests SSE endpoints at scale with k6 - measures concurrent-stream capacity, connection churn, and server memory pressure. Covers the HTTP/1.1 6-connection-per-origin browser ceiling vs HTTP/2 multiplexing, a custom k6 SSE client built on ReadableStream, and threshold gates for TTFB and data throughput. Use when validating whether a server can sustain N concurrent EventSource connections without connection starvation or memory growth.

stomp-amqp-tests

Tests STOMP over WebSocket (Spring, ActiveMQ, RabbitMQ Web STOMP) and AMQP 0-9-1 (RabbitMQ Java client) - frame connect/subscribe/send/ack sequences, ack modes (auto/client/client-individual), exchange and queue declarations, binding routing, Testcontainers RabbitMQ broker, and delivery assertion. Use when validating enterprise Spring or RabbitMQ messaging stacks before deploy.

webhook-replay-tests

Tests inbound webhook receivers for replay-attack resistance: capture incoming webhook payloads + headers, replay against the receiver under test, validate the Standard Webhooks signature scheme (svix-id + svix-timestamp + svix-signature, HMAC-SHA256 over `{id}.{timestamp}.{payload}`), svix-id idempotency dedup, and 5-minute timestamp-window enforcement by signing fixtures at runtime. Does NOT cover outbound delivery, retry-on-5xx, or failure-event exhaustion - those belong to an outbound webhook delivery harness. Use when testing the receiving side of a webhook integration.

websocket-tests

Test WebSocket protocol behavior - opening handshake (HTTP Upgrade with Sec-WebSocket-Key + Sec-WebSocket-Version: 13), control frames (ping 0x9 / pong 0xA / close 0x8), close-frame status codes (1000 normal, 1001 going-away, 1006 abnormal, 1011 server error), subprotocol negotiation, backpressure, and reconnect with jitter. Works with ws (Node), websockets (Python), or Playwright frame inspection per language. Use when a feature holds a long-lived WebSocket open and reconnect, close-code, or backpressure behavior is unverified.