Overview of the testing infrastructure and patterns used in Strait.
Testing is a core part of Strait development process. We use a combination of unit, integration, E2E, and fuzz tests to ensure system reliability.
Test Infrastructure
Shared testing utilities are located in apps/strait/internal/testutil/:
testdb.go: Helpers for spinning up ephemeral PostgreSQL instances usingtestcontainers-go.testredis.go: Helpers for ephemeral Redis instances.factory.go: Data factories for generating test entities (jobs, runs, workflows).assert.go: Custom assertion helpers for common domain checks (usessamber/lofor collection queries).cmp.go: Structural comparison helpers powered bygoogle/go-cmpfor rich, human-readable diffs.
Running Tests
Unit Tests
Unit tests are fast and do not require external dependencies.
cd apps/strait && go test ./...Integration Tests
Integration tests require Docker to run testcontainers. They are marked with the integration build tag.
cd apps/strait && go test -tags integration ./...End-to-End (E2E) Tests
E2E tests verify the entire system flow, from API request to job execution.
cd apps/strait && go test -tags integration ./internal/e2e/...Fuzz Tests
We use Go's native fuzzing to test the robustness of the FSM transitions.
cd apps/strait && go test -fuzz FuzzFSMTransition ./internal/domain/Benchmarks
Performance benchmarks for critical paths like the job queue and workflow engine.
cd apps/strait && go test -bench . ./internal/...Event Trigger Benchmarks
Integration-tagged benchmarks for event trigger database operations:
# Requires DATABASE_URL and 'integration' build tag
go test -tags integration -bench . ./internal/e2e/...Available benchmarks:
BenchmarkListExpiredEventTriggers— Measures reaper query performance with 1000 triggersBenchmarkListByKeyPrefix— Measures prefix matching query performance with 1000 triggers
Load Tests
Strait has two load testing approaches that cover different concerns.
Vegeta Load Tests (HTTP)
Vegeta-based tests in apps/strait/test/loadtest/ exercise every API route at scale using three attack modes: baseline (fixed-rate SLA validation), stress (high-rate ceiling discovery), and spike (linear ramp). Tests require the loadtest and integration build tags and spin up a full API server with testcontainers PostgreSQL.
cd apps/strait && go test -tags "loadtest,integration" -timeout=30m ./test/loadtest/...Test files cover all API surface areas:
jobs_test.go— CRUD, listing, filtering, pagination, batch operationstriggers_test.go— Single, bulk, idempotent, delayed, priority triggersruns_test.go— Listing, filtering, status transitions, replay, annotationsworkflows_test.go— CRUD, triggering, step run listing, concurrent operationssdk_test.go— Heartbeat, logging, checkpoints, progress, continuationwebhooks_test.go— Subscription managementrbac_test.go— API key CRUD, scoped access enforcementevents_test.go— Event sending and listingjob_groups_test.go— Group CRUD, job assignmentenvironments_test.go— Environment CRUD, variable managementmixed_test.go— Cross-resource concurrent operationsedge_cases_test.go— Malformed payloads, boundary values, conflict handling
Integration Load Tests (Go)
Go-native load tests in apps/strait/internal/e2e/ test internal subsystem throughput using testcontainers. They exercise queue, worker, webhook, workflow, rate limiting, store, and full pipeline operations at configurable volumes.
cd apps/strait && go test -tags integration -run "TestLoad" -timeout=30m ./internal/e2e/...Control volume via LOADTEST_VOLUME_TIER:
cd apps/strait && LOADTEST_VOLUME_TIER=large go test -tags integration -run "TestLoad" ./internal/e2e/...Available tiers: default (500 items), large (5,000 items), extreme (10,000 items).
Test files:
load_queue_test.go— Enqueue/dequeue throughput, concurrent operations, FSM transitionsload_worker_test.go— Bulk trigger, concurrent triggers, SDK heartbeat floodload_webhook_test.go— Subscription CRUD throughput, delivery listingload_workflow_test.go— Workflow creation, triggering, concurrent operationsload_ratelimit_test.go— Burst, sustained, mixed rate limit scenariosload_store_test.go— Direct store throughput for jobs, events, FSM, paginationload_pipeline_test.go— Full trigger-dequeue-complete lifecycle pipelines
Event Trigger Load Tests
Legacy integration-tagged load tests for event trigger throughput:
cd apps/strait && go test -tags integration -run TestEventTriggerLoad ./internal/e2e/...Available load tests:
TestEventTriggerLoadCreate— Sequential create throughput (500 triggers)TestEventTriggerLoadSendConcurrent— 50 concurrent event sends
CI Workflow
Load tests run via the Load Tests GitHub Action (.github/workflows/loadtest.yml). This workflow is manually triggered via workflow_dispatch with configurable volume tier and duration. It runs Vegeta and integration load tests in parallel jobs, generates markdown summaries, and publishes results to the GitHub Actions step summary.
gh workflow run "Load Tests" --field volume_tier=largeAdvanced Testing Tools
Race Detector
Always run tests with the race detector enabled in CI and during local development of concurrent features.
cd apps/strait && go test -race ./...Code Coverage
Generate a coverage report to identify untested paths.
cd apps/strait
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.outTest Patterns
Table-Driven Tests
We prefer table-driven tests for testing multiple scenarios with the same logic.
func TestCalculateBackoff(t *testing.T) {
tests := []struct {
name string
attempt int
expected time.Duration
}{
{"first attempt", 1, 1 * time.Second},
{"second attempt", 2, 2 * time.Second},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// ...
})
}
}Parallel Execution
Use t.Parallel() in unit tests to speed up execution.
Structural Assertions with go-cmp
Use testutil.AssertEqual for comparing complex structs. It produces rich, human-readable diffs on failure instead of opaque assertion messages.
func TestCreateJob(t *testing.T) {
got := store.GetJob(ctx, id)
testutil.AssertEqual(t, got, want)
}Available helpers in apps/strait/internal/testutil/cmp.go:
AssertEqual(t, got, want, ...cmp.Option): Structural comparison with rich diff output.AssertJSONEqual(t, a, b []byte): JSON-aware comparison that ignores key ordering.EquateEmpty(): Treats nil slices/maps as equal to empty ones.IgnoreFields(typ, ...names): Skips specified fields during comparison (useful for timestamps, IDs).
Testcontainers
We use testcontainers-go to manage the lifecycle of PostgreSQL and Redis during integration tests. This ensures a clean state for every test suite.
Mock Patterns
For packages like apps/strait/internal/api and apps/strait/internal/scheduler, we use mock stores with configurable function fields:
type mockAPIStore struct {
getEventTriggerByEventKeyFn func(ctx context.Context, key string) (*domain.EventTrigger, error)
// ... other store methods
}Each method delegates to its function field if set, falling back to sensible defaults. This enables precise control over test behavior without implementing the full store interface.
For pubsub testing, mockPublisher supports configurable subscribeFn that returns real pubsub.Subscription instances with channels, enabling SSE stream testing without Redis.
Test Fixtures
Static test data and JSON payloads are stored in testdata/ directories within their respective packages.