runn
runn is a scenario-based testing tool that supports gRPC, HTTP/REST, databases, and more. It uses YAML runbooks to define multi-step test scenarios with assertions.
Why runn for Connectum:
- gRPC testing with server reflection (no proto files needed)
- HTTP testing for ConnectRPC endpoints
- Built-in assertion engine (expr-lang)
- Single binary, CI-friendly
- Docker image available
Reference
The runbook syntax, the gRPC runner and the assertion functions are documented in the runn README. The examples below cover the Connectum-specific parts: reflection, health checks, and auth headers.
Installation
brew install k1LoW/tap/runngo install github.com/k1LoW/runn/cmd/runn@latestaqua g -i k1LoW/runndocker pull ghcr.io/k1low/runn:latestTesting gRPC Services
Connectum services expose gRPC endpoints. Server reflection can be enabled via Reflection() protocol. runn can discover services automatically without proto files.
For the plaintext gRPC runbooks, use allowHTTP1: false and Reflection(). For HTTP/1.1 runbooks, restart with allowHTTP1: true. One plaintext listener does not serve both protocols; see Transport Matrix.
Create tests/grpc-greeter.yml:
desc: Greeter service — gRPC
runners:
greq: grpc://localhost:5000
steps:
say_hello:
desc: SayHello returns greeting
greq:
greeter.v1.GreeterService/SayHello:
message:
name: Alice
test: |
current.res.status == 0 &&
current.res.message.message == 'Hello, Alice!'Run it:
--grpc-no-tls selects plaintext h2c; the tool otherwise attempts TLS.
runn run --grpc-no-tls tests/grpc-greeter.ymlServer Reflection
When Server Reflection is enabled via protocols: [Reflection()], runn discovers services automatically. No protos: or importPaths: configuration needed.
With Proto Files
If reflection is disabled, point runn to your proto definitions:
runners:
greq:
addr: localhost:5000
importPaths:
- proto
protos:
- greeter.protogRPC Metadata
Pass metadata (headers) with requests:
steps:
with_metadata:
greq:
greeter.v1.GreeterService/SayHello:
headers:
authorization: "Bearer my-token"
x-request-id: "test-123"
message:
name: Bob
test: current.res.status == 0Testing ConnectRPC HTTP Endpoints
Connectum services also accept HTTP/1.1 requests via the ConnectRPC protocol.
Create tests/http-greeter.yml:
desc: Greeter service — ConnectRPC HTTP
runners:
req: http://localhost:5000
steps:
say_hello:
desc: SayHello via HTTP POST
req:
/greeter.v1.GreeterService/SayHello:
post:
headers:
Content-Type: application/json
body:
application/json:
name: Bob
test: |
current.res.status == 200 &&
current.res.body.message == 'Hello, Bob!'Testing Health Checks
Connectum exposes both gRPC and HTTP health check endpoints.
Create tests/health.yml:
This combined runbook assumes two running listeners: h2c on port 5000 for gRPC, HTTP/1.1 on port 5001 for HTTP. Register Healthcheck({ httpEnabled: true }) on each and mark the application services SERVING; include reflection on the gRPC listener. With only one listener, run the matching health step separately.
desc: Health check endpoints
runners:
greq: grpc://localhost:5000
req: http://localhost:5001
steps:
grpc_health:
desc: gRPC Health Check
greq:
grpc.health.v1.Health/Check:
message: {}
test: |
current.res.status == 0 &&
current.res.message.status == 1
http_health:
desc: HTTP /healthz
req:
/healthz:
get:
headers:
Accept: application/json
test: |
current.res.status == 200 &&
current.res.body.status == 'SERVING' &&
current.res.body.service == 'overall'Validation Testing
Test that protovalidate constraints reject invalid input:
desc: Validation rejects empty name
runners:
greq: grpc://localhost:5000
steps:
empty_name:
desc: SayHello with empty name returns INVALID_ARGUMENT
greq:
greeter.v1.GreeterService/SayHello:
message:
name: ""
test: current.res.status == 3gRPC status code 3 = INVALID_ARGUMENT.
Multi-Step Scenarios
Chain steps together using variable references:
desc: Multi-step gRPC scenario
runners:
greq: grpc://localhost:5000
vars:
username: Charlie
steps:
greet:
desc: Greet user
greq:
greeter.v1.GreeterService/SayHello:
message:
name: "{{ vars.username }}"
test: current.res.status == 0
verify_message:
desc: Verify greeting format
test: |
steps.greet.res.message.message == 'Hello, Charlie!'Variables and Environment
desc: Environment-driven tests
vars:
host: ${GRPC_HOST:-localhost:5000}
token: ${AUTH_TOKEN}
runners:
greq: "grpc://{{ vars.host }}"
steps:
authenticated_call:
greq:
myapp.v1.MyService/GetData:
headers:
authorization: "Bearer {{ vars.token }}"
message: {}
test: current.res.status == 0Streaming RPCs
Test server-streaming responses:
steps:
server_stream:
greq:
myapp.v1.MyService/ListItems:
message:
limit: 10
test: |
current.res.status == 0 &&
len(current.res.messages) > 0For client-streaming, send multiple messages:
steps:
client_stream:
greq:
myapp.v1.MyService/SendBatch:
messages:
- data: "item-1"
- data: "item-2"
- data: "item-3"
test: current.res.status == 0TLS / mTLS
runners:
greq:
addr: grpc.example.com:443
tls: true
cacert: certs/ca.pem
cert: certs/client.pem
key: certs/client-key.pemCI/CD Integration
GitHub Actions
This excerpt assumes your checkout has a committed npm lockfile, the scripts from the Quickstart, and a server configured for h2c with HTTP health enabled. Run HTTP/1.1 runbooks in a separate job with the matching server configuration.
jobs:
api-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '26'
- run: npm ci
- run: npm run build:proto
- name: Install runn
run: |
brew install k1LoW/tap/runn
- name: Start service
run: node src/index.ts &
- name: Wait for service
run: |
for i in $(seq 1 30); do
if curl -fsS --http2-prior-knowledge http://localhost:5000/healthz; then
exit 0
fi
sleep 1
done
exit 1
- name: Run API tests
run: runn run --grpc-no-tls tests/grpc-greeter.ymlDocker Compose
Runbooks execute inside the api-tests container, where localhost refers to that container. For the plaintext gRPC runbook, override its runner to use the Compose app service; the runbook's localhost default remains usable outside Compose. This example runs only the gRPC runbook and assumes the app listens for plaintext gRPC on port 5000.
services:
app:
build: .
ports:
- "5000:5000"
api-tests:
image: ghcr.io/k1low/runn:latest
depends_on:
- app
volumes:
- ./tests:/books
command:
- run
- --grpc-no-tls
- --runner
- greq:grpc://app:5000
- /books/grpc-greeter.ymlFor the HTTP/1.1 runbook, restart the app with allowHTTP1: true and run runn run --runner req:http://app:5000 /books/http-greeter.yml instead. The same plaintext listener does not serve both gRPC and HTTP/1.1 modes.
CLI Reference
# Run all test scenarios
runn run --grpc-no-tls tests/**/*.yml
# Run with verbose output
runn run --grpc-no-tls --verbose tests/grpc-greeter.yml
# List available scenarios
runn list tests/**/*.yml
# Generate a runbook from a command
runn new -- grpcurl -plaintext localhost:5000 greeter.v1.GreeterService/SayHelloRecommended Test Structure
tests/
├── api/
│ ├── grpc-greeter.yml # gRPC endpoint tests
│ ├── http-greeter.yml # ConnectRPC HTTP tests
│ ├── health.yml # Health check tests
│ └── validation.yml # Validation constraint tests
└── scenarios/
├── user-flow.yml # Multi-step user scenarios
└── error-handling.yml # Error response testsRelated
- Testing Overview -- back to overview
- scenarigo -- alternative testing tool
- Server Reflection -- enable reflection for runn
- Health Checks -- health check endpoints to test