- Tests: use
make testas the default. Only usego test ...when you must pass specific flags (e.g.-run,-count,-race, build tags, etc.) or need to debug a specific test in isolation. - Lint: use
make lintas the default. Only callgolangci-lint ...directly when you must pass specific flags. - Always run test and lint prior to considering a task complete, unless told otherwise (or if you did not touch any Go code/tests).
- If your execution environment has a sandbox/permission model, run
make testandmake lintunsandboxed (full permissions) so results match local dev and CI.
- Run all tests with race detector:
make test/race - Run single test:
cd $MODULE && go test ./path/to/package -run TestName - Run benchmark:
make bench - Generate sqlc:
make generate. Use this any time a.sqlfile has been modified and we need to then regenerate.sql.gofiles from it. - Run tidy when deps change:
make tidy
- Imports: use gci sections - Standard, Default, github.com/riverqueue.
- Formatting: use gofmt, gofumpt, goimports.
- JSON tags: use snake_case for JSON field tags.
- Dependencies: minimize external dependencies beyond standard library and pgx.
- SQL access (non-test code): avoid ad-hoc SQL strings in library/runtime code. Add or extend a sqlc query, regenerate with
make generate, and expose it through the driver interface. Keep direct SQL for tests and benchmark/admin utilities only (for example,pg_stat_statementsandVACUUM). - Driver interface stability: treat
riverdriveras an internal adapter seam, not as an official external API. Its package comments explicitly say it should not be implemented or invoked by user code, and changes there are not considered semver-breaking. Do not preserve driver-interface methods or semantics for outside consumers; add, remove, or reshape them as needed to preserve or improve user-facing functionality. - Cross-driver driver tests: treat
riverdriver/riverdrivertestas the shared conformance suite for driver behavior. When changingriverdriveror a concrete driver, update or extendriverdrivertestso the intended semantics are exercised across drivers, not only in a single driver-specific test. - Error handling: prefer context-rich errors; review linting rules before disabling them.
- Testing: use require variants instead of assert.
- Helpers: use
Funcsuffix for function variables, notFn. - Documentation: include comments for exported functions and types.
- Naming: use idiomatic Go names; see
.golangci.yamlfor allowed short variable names.
- Package names are lowercase, short, and representative; avoid
common,util, or overly broad names. - Use singular package names; avoid plurals like
httputils. - Keep import paths clean; avoid
src/,pkg/, or other repo-structure leakage. - Organize by responsibility instead of
models/typesbuckets; keep types close to usage. - Do not export identifiers from
mainpackages that only build binaries. - Add package docs, and use
doc.gowhen documentation is long.
Alphabetization is important when adding new code (do not reorganize existing code unless asked).
- Types should be sorted alphabetically by name.
- Struct field definitions on a type should be sorted alphabetically by name, unless there is a good reason to deviate (examples: ID fields first, grouping mutexed fields after a mutex, etc.).
- When declaring an instance of a struct, fields should be sorted alphabetically by name unless a similar deviation is justified.
- When defining methods on a type, they should be sorted alphabetically by name.
- Constructors should come immediately after the type definition.
- Keep all methods for a type grouped together, immediately after the type definition and any constructor(s), organized alphabetically by name. Do not intersperse methods with other types or functions, except in special cases where a small utility type is needed to support a method and not used elsewhere.
- In unit tests, the outer test blocks should be sorted alphabetically by name. Inner test blocks should also be sorted alphabetically by name within the outer block.
This repo uses a parallel test bundle pattern (inspired by Brandur's write-up: https://brandur.org/fragments/parallel-test-bundle) to keep parallel subtests isolated and setup/fixtures DRY.
- Always opt into parallel:
- Top-level tests: the first statement in every
TestXxxshould bet.Parallel(). - Subtests: the first statement in every
t.Run(..., func(t *testing.T) { ... })should bet.Parallel(), unless the subtest is intentionally non-parallel and includes a short comment explaining why.
- Top-level tests: the first statement in every
- Statement ordering and spacing:
- Top-level tests: use
t.Parallel(), then a blank line, then test preamble (ctx,setup, helpers), then a blank line before subtests/assertions. - Subtests: use
t.Parallel(), then a blank line, then subtest preamble. - If a subtest calls
setup(...), prefer one blank line after the setup assignment before assertions/actions. - For tiny/obvious subtests (one short statement after
t.Parallel()orsetup(...)), omitting one of these blank lines is acceptable.
- Top-level tests: use
- Context and setup ordering:
- If
setupneedsctx(setup(ctx, t)), assign/derivectxbefore callingsetup. - If
setupdoes not needctx(setup(t)), callsetupfirst and derive specialized contexts (WithCancel,WithTimeout) close to where they are used. - Avoid creating/deriving
ctxfar from usage unless shared setup requires it.
- If
- Prefer local bundles:
- Define a
type testBundle struct { ... }inside theTestXxxfunction containing the system under test and any fixtures frequently used across subtests. - Each parallel subtest should call
setup(t)to get a fresh bundle. Avoid sharing mutable state across parallel subtests.
- Define a
setuphelper rules:- Define
setupas a local closure in the test:setup := func(t *testing.T) *testBundle { ... }- Always call
t.Helper()at the top ofsetup.
- Only accept a context parameter if it is needed:
- Default:
setup(t)should not takectx. - If setup must derive/seed a context: prefer returning it:
setup := func(t *testing.T) (*testBundle, context.Context). - If setup must be passed an existing context: accept
ctxas the first parameter:setup := func(ctx context.Context, t *testing.T) *testBundle.
- Default:
- Keep
setupdeterministic and self-contained; it should only use the*testing.T(andctxif explicitly required) passed in.
- Define
- Test signal instrumentation:
- Prefer
rivershared/testsignal.TestSignalin a...TestSignalsstruct with anInit(tb)helper over ad hocchan struct{}fields in spies/fakes. - Keep test-signal structs zero-value by default and call
Init(t)only in tests that need to observe those signals. - Wait for async events with
WaitOrTimeout(). For negative assertions, useRequireEmpty()orWaitC()with a timeout select. - Avoid custom channel signaling helpers and hand-managed channel capacities unless there is a specific, documented reason.
- Prefer
Template:
func TestThing(t *testing.T) {
t.Parallel()
type testBundle struct {
// Put SUT + common fixtures here.
}
setup := func(t *testing.T) *testBundle {
t.Helper()
return &testBundle{}
}
t.Run("CaseName", func(t *testing.T) {
t.Parallel()
bundle := setup(t)
// ... use `bundle` in assertions/actions ...
})
t.Run("CaseNameWithCtxRequiredBySetup", func(t *testing.T) {
t.Parallel()
setupWithCtx := func(ctx context.Context, t *testing.T) *testBundle {
t.Helper()
_ = ctx
return &testBundle{}
}
ctx := context.Background()
bundle := setupWithCtx(ctx, t)
// ... use `bundle` in assertions/actions ...
})
}