> ## Documentation Index
> Fetch the complete documentation index at: https://engineering.unkey.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing

> Testing standards and patterns for Unkey

## Why we test

Tests exist to give confidence. Confidence to ship changes quickly, confidence that refactoring will not break production, confidence that the system behaves as expected. A test suite with 90 percent coverage that misses critical edge cases is less valuable than one with 60 percent coverage that catches real bugs.

We prioritize quality over quantity. A single well-designed test that validates complex business logic is worth more than a dozen tests that exercise trivial code paths. When writing tests, ask what could go wrong in production that this test would catch.

Treat test code as production code. Tests are part of the system contract, not a safety net added after implementation. They must be readable, typed, deterministic, reviewed with the same care as application code, and maintained when behavior changes.

Every test must make its guarantee clear to the reader. The guarantee is the production behavior that would break if the test failed. A future reader must be able to understand what risk the test covers without reverse engineering setup, mocks, or fixtures.

Document non-obvious guarantees with docstrings or comments. Use them when a test protects a security property, regression, invariant, concurrency condition, failure mode, or business rule that the test name cannot fully explain.

```go theme={"theme":"kanagawa-wave"}
// TestTokenParserRejectsAlgorithmConfusion guarantees that tokens signed with
// one algorithm cannot be accepted under another algorithm. This protects the
// verifier from accepting attacker-controlled headers as trusted policy.
func TestTokenParserRejectsAlgorithmConfusion(t *testing.T) {
    // ...
}
```

Do not document obvious mechanics. A comment that says "creates a workspace" above `createWorkspace(t)` adds noise. A comment that explains why cross-workspace access must return 404 instead of 403 documents a guarantee.

## What to test

Invest testing effort where bugs would hurt most.

**High value targets:** Business logic with complex conditionals, error handling paths, concurrent code with race potential, security sensitive operations, data transformations that could silently corrupt.

**Lower value targets:** Simple getters and setters, straightforward pass-through functions, code that delegates to well-tested libraries.

**Skip entirely:** Tests that verify the programming language works.

Ask what bug this test would catch that the compiler, a code review, or a more meaningful test would not.

### Testing observability

Do not verify every log line or metric increment. Test metrics that drive alerts or SLOs. Test that error conditions produce the logs operators need for debugging.

```go theme={"theme":"kanagawa-wave"}
func TestRateLimiter_EmitsRejectionMetric(t *testing.T) {
    collector := &testMetricCollector{}
    limiter := NewRateLimiter(Config{Limit: 1}, collector)

    limiter.Allow()
    limiter.Allow()

    require.Equal(t, 1, collector.Count("rate_limit_rejected_total"))
}
```

## Go testing

All Go tests use `github.com/stretchr/testify/require` for assertions.

Run normal Go test suites with Rask through mise. Rask keeps package-level test
runs fast while preserving the standard Go test behavior.

## Test organization

Tests live alongside the code they test. A file `cache.go` has its tests in `cache_test.go` in the same directory.

For integration tests that require substantial setup or external dependencies, create an `integration/` subdirectory when it improves clarity.

Organize tests around guarantees, not implementation details. File names, test names, table cases, and helper names must help a reader answer which behavior is protected and why it matters.

Use package structure, test names, and helper names to communicate test scope.
Keep unit tests close to the package under test. Put expensive cross-service
tests in an `integration/` subdirectory when that makes the boundary clearer.

## Writing test helpers

Every helper function must call `t.Helper()` as its first line.

```go theme={"theme":"kanagawa-wave"}
func createTestWorkspace(t *testing.T) *Workspace {
    t.Helper()
    ws, err := db.CreateWorkspace(ctx, "test-workspace")
    require.NoError(t, err)
    return ws
}
```

## Resource cleanup

Tests that acquire resources must clean them up. Use `t.Cleanup()` instead of `defer`.

```go theme={"theme":"kanagawa-wave"}
func TestWithDatabase(t *testing.T) {
    db := setupTestDatabase(t)
    t.Cleanup(func() {
        db.Close()
    })
}
```

## Running tests

During development, run tests for the package you are working on:

```bash theme={"theme":"kanagawa-wave"}
mise exec -- rask ./pkg/cache
```

Before pushing, run the full test suite:

```bash theme={"theme":"kanagawa-wave"}
mise run test
```

## What is next

Use these guides for deeper patterns:

* [Unit tests](/contributing/quality/testing/unit-tests)
* [Integration tests](/contributing/quality/testing/integration-tests)
* [Anti-patterns](/contributing/quality/testing/anti-patterns)
