Table of contents
How to test code that uses nurago, and how nurago tests itself.
Test Helpers
testutil collects the helpers that make otherwise awkward branches testable. It is intended for use in tests only.
reader := testutil.NewErrorReader("read failed") // Read always fails
closer := testutil.NewErrorCloser("close failed") // Close always fails
output := testutil.CaptureOutput(t, func() {
fmt.Println("hello")
})
h := testutil.RouterWithHandler(http.MethodGet, "/health", handler)
normalized := testutil.ReplaceDateTime(responseBody, "<DATETIME>")
normalized = testutil.ReplaceUnixTimestamp(normalized, "<UNIX_TS>")
The error reader and closer exercise I/O failure paths that are hard to trigger otherwise. CaptureOutput redirects stdout, stderr, and the default logger for the duration of a call. RouterWithHandler builds a real httprouter-backed handler, which is what a handler reading path parameters needs. ReplaceDateTime and ReplaceUnixTimestamp normalise generated timestamps so a JSON response can be compared against a fixed golden value.
Testing Without the Dependency
Every client package accepts an injected implementation, so a test does not need the live dependency and no network is dialled: WithRedisClient, WithValkeyClient, WithS3Client, WithHTTPClient, WithTransport, WithReverseProxy. When a client is injected, the address validation and AWS configuration loading are skipped as well.
Tests can substitute the logger and the metrics client as well:
metrics.Defaultis a no-op implementation of the metrics contract that still returns a working SQL connection and a metrics handler answering “OK”, so a service under test needs no metrics backend.logutilhas a discard format, andslog.New(slog.DiscardHandler)works anywhere a logger option is accepted, which keeps test output readable.
The SQL examples in the repository use go-sqlmock in place of a database, which is what makes the examples on the transaction package pages deterministic.
Mocks
Interface mocks are generated: make generate removes every *mock_test.go and rebuilds them with go generate. Adding a method to an interface means regenerating, never hand-editing the mock.
The QA Pipeline
From a clone of the repository:
make x # everything from scratch: format, clean, deps, generate, gendoc, qa, example
make qa # linters, vulnerability check, unit tests, coverage
make test # unit tests only
make test runs with -shuffle=on, -race, atomic coverage, and every benchmark executed once. An ordering assumption, a data race, or a broken benchmark therefore fails the same run. make linter runs golangci-lint with no per-linter issue cap, and make govulncheck checks dependencies for known vulnerabilities. Reports land in target/report/, the HTML coverage report among them.
To run the same flow inside a container, using resources/docker/Dockerfile.dev:
make dbuild
Benchmarks
Allocation regressions are gated rather than eyeballed:
make bench # run benchmarks, store results in target/report/bench.txt
make benchbase # store the baseline in target/report/bench_base.txt
make benchcmp # compare allocation counts against a git ref (BENCHBASE=ref, default main)
make benchgate # compare against a baseline file (BASELINE=path)
make benchcmp and make benchgate fail on regression, so a change that adds an allocation to a hot path is caught in CI.
Documentation Checks
The package READMEs and llms.txt in the repository are generated from the source documentation. make gendoc regenerates them, and make gendoccheck fails when they are out of date. The package pages on this site are generated from those same READMEs.
Previous: /docs/data-and-messaging/
Overview: /docs/