Strait Docs
Development

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 using testcontainers-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 (uses samber/lo for collection queries).
  • cmp.go: Structural comparison helpers powered by google/go-cmp for 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 triggers
  • BenchmarkListByKeyPrefix — 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 operations
  • triggers_test.go — Single, bulk, idempotent, delayed, priority triggers
  • runs_test.go — Listing, filtering, status transitions, replay, annotations
  • workflows_test.go — CRUD, triggering, step run listing, concurrent operations
  • sdk_test.go — Heartbeat, logging, checkpoints, progress, continuation
  • webhooks_test.go — Subscription management
  • rbac_test.go — API key CRUD, scoped access enforcement
  • events_test.go — Event sending and listing
  • job_groups_test.go — Group CRUD, job assignment
  • environments_test.go — Environment CRUD, variable management
  • mixed_test.go — Cross-resource concurrent operations
  • edge_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 transitions
  • load_worker_test.go — Bulk trigger, concurrent triggers, SDK heartbeat flood
  • load_webhook_test.go — Subscription CRUD throughput, delivery listing
  • load_workflow_test.go — Workflow creation, triggering, concurrent operations
  • load_ratelimit_test.go — Burst, sustained, mixed rate limit scenarios
  • load_store_test.go — Direct store throughput for jobs, events, FSM, pagination
  • load_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=large

Advanced 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.out

Test 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.

Was this page helpful?

On this page