Reference
The mental model behind gotest. Learn the conventions once — the rest follows.
- Conceptual Model — how Go constructs map to spec concepts
- Naming Conventions — suffixes, prefixes, and lifecycle methods
- Suite Lifecycle — execution order, hooks, and parallel modes
- Test DSL — When/It, data-driven tests, snapshots, polling
- Fixtures — package fixtures, DAG ordering, shared fixtures
- Assertions — type-safe generics for equality, errors, collections
- Configuration — project file, suite config, fixture config
- Coverage — statement-weighted calculation and thresholds
- Benchmarking — benchmark suites, baselines, deltas, and the gate
- Fuzzing — fuzz targets, struct inputs, sessions, crashers
- CLI Reference — commands, flags, go test passthrough
- Linter — rule tiers, suppression, exit codes
- CI Integration — summary output, GitHub Action, PR annotations
Conceptual Model
Test suites are behavioral specifications. Every level of the hierarchy maps to a concept.
| Go construct | Spec concept | Example |
|---|---|---|
struct | Subject | UserServiceTestSuite |
method | Capability | TestCreate |
When() | Context | "email is new" |
It() | Behavior | "creates the user" |
Each() | Variants | "standard format", "missing @" |
Naming conventions at the struct and method level, plus string descriptions in When and It, form a complete specification. The tool generates the wiring and can render the full spec in human-readable form:
This isn't a separate reporting layer — it's the same test hierarchy rendered differently. The spec view and the test output always agree because they come from the same source.
Naming Conventions
The naming conventions are the entire API. There are no config files, struct tags, or annotations.
Types
| Pattern | Meaning |
|---|---|
*TestSuite | Test suite. Each generates a func Test* entry point. |
*Fixture | Package fixture. Setup each suite of a package can bind; set up per suite. |
*SharedFixture | Cross-package fixture. Runs in a subprocess, state transfers via JSON. |
Methods
| Method | Meaning |
|---|---|
Test* | Test case. Becomes a subtest. |
BeforeAll | Runs once before all tests in the suite. |
AfterAll | Runs once after all tests. Registered as cleanup, so it always runs. |
BeforeEach | Runs before every test case. |
AfterEach | Runs after every test case. Deferred, so it runs even on fatal errors. |
SuiteGuard() | Returns a reason to skip the suite, or empty string to run. Evaluated at runtime, before the suite's fixtures are set up. |
SuiteConfig() | Returns timeout, parallelism, and fail-fast settings. |
FixtureConfig() | Returns timeout and retry settings for package fixtures. |
SharedFixtureConfig() | Returns timeout and retry settings for shared fixtures. |
Hydrate / Dehydrate | Reconstruct / clean up local resources in shared fixtures after state transfer. |
Prefixes
| Prefix | Effect | Use case |
|---|---|---|
F_ | Focus — only focused items run | Tight iteration during development |
X_ | Exclude — always skipped | Temporarily disable a flaky test |
F_ and X_ work on both types and methods. X_ takes precedence over F_. Use --ci to fail the build if any focus prefix is committed and to enable snapshot read-only mode. CI mode is auto-detected from the CI environment variable.Suite Lifecycle
Predictable execution order with guaranteed cleanup — even on panics and fatal errors.
Execution order
All hooks are optional. Unimplemented hooks are no-ops. AfterAll is registered before BeforeAll runs, so cleanup happens even if setup fails. AfterEach is deferred, so it runs even on t.Fatal().
Hook signatures
Every hook accepts either *gotest.T (full DSL access) or *testing.T (plain stdlib). Mix freely within the same suite:
func (s *MySuite) BeforeAll(t *gotest.T) {} // full DSL func (s *MySuite) BeforeEach(t *testing.T) {} // plain stdlib func (s *MySuite) TestPlain(t *testing.T) {} // no gotest import needed func (s *MySuite) TestRich(t *gotest.T) {} // When, It, MatchSnapshot
Per-test context
When BeforeEach returns a value, each test receives its own isolated context. This enables safe method-level parallelism without shared mutable state:
func (s *MySuite) BeforeEach(t *gotest.T) *TestCtx { return &TestCtx{conn: s.pool.Acquire()} } func (s *MySuite) AfterEach(t *gotest.T, ctx *TestCtx) { ctx.conn.Release() } func (s *MySuite) TestCreate(t *gotest.T, ctx *TestCtx) { // ctx.conn is unique to this test }
Parallel execution
Suite-level: Each suite runs as a separate subprocess — full process isolation with zero shared state between suites. This is automatic.
Method-level: Opt-in via SuiteConfig{Parallel: true}. Per-test state lives in the context a returning BeforeEach provides, never on the shared suite struct; the generator rejects a BeforeEach that returns nothing in a parallel suite, and a suite without per-test state needs none.
Test DSL
Methods on *gotest.T for structuring tests. Every method maps to t.Run — the DSL adds semantics, not runtime machinery.
When / It
When groups context. It specifies behavior. Nest them freely to build a specification tree:
func (s *MySuite) TestCreate(t *gotest.T) { t.When("email is new", func(w *gotest.T) { w.It("creates the user", func(it *gotest.T) { err := s.svc.Create(ctx, validUser) gotest.NoError(it, err) }) }) t.When("email already exists", func(w *gotest.T) { w.It("returns ErrDuplicate", func(it *gotest.T) { s.svc.Create(ctx, validUser) err := s.svc.Create(ctx, validUser) gotest.ErrorIs(it, err, ErrDuplicate) }) }) }
Data-driven tests
Iterator API — range over test entries with compile-time type safety:
for it, tc := range gotest.Each(t, []struct { Desc string Input string Want int }{ {Desc: "single digit", Input: "5", Want: 5}, {Desc: "negative", Input: "-3", Want: -3}, }) { gotest.Equal(it, tc.Want, parse(tc.Input)) }
Desc or Name field for the test name, falls back to #0, #1, etc.Snapshot testing
Capture expected output once, verify it on every subsequent run:
gotest.MatchSnapshot(t, render(input)) // auto-named from test path gotest.MatchSnapshot(t, render(other), "variant") // explicit snapshot name
Snapshots are stored in testdata/__snapshots__/ next to the test file. On first run, the snapshot is created. On subsequent runs, the output is compared and failures include a diff. Snapshot entries are written in deterministic order and are thread-safe — parallel test methods can use MatchSnapshot without coordination.
Update all snapshots with gotest --update-snapshots ./....
Async polling
Package-level functions for polling conditions. Callbacks receive *gotest.R — an assertion recorder that captures failures without propagating them:
gotest.Eventually(t, 5*time.Second, 100*time.Millisecond, func(poll *gotest.R) { result, err := s.store.Get("key") gotest.NoError(poll, err) gotest.Equal(poll, "completed", result.Status) })
gotest.Consistently(t, 2*time.Second, 100*time.Millisecond, func(poll *gotest.R) {
gotest.True(poll, s.cache.IsValid())
})On each tick the callback runs; if any assertion fails, it retries on the next tick. On timeout, the last failure is reported. Consistently inverts the logic — it fails on the first assertion failure.
Helpers
| Method | Description |
|---|---|
t.T() | Returns the underlying *testing.T. Use when you need to pass it to third-party libraries. |
t.Context() | Returns the test context. Cancelled when the test ends, carries the test deadline. |
Fixtures
Expensive setup — databases, containers, external services — defined once, wired into every suite that needs it.
Package fixtures
Any struct ending in Fixture is a package fixture. Suites reference it via a named pointer field:
type E2EFixture struct { Pool *pgxpool.Pool } func (f *E2EFixture) BeforeAll(ctx context.Context) error { // start database, populate f.Pool } type BatchTestSuite struct { Fixture *E2EFixture // automatically wired }
Fixture hooks use (ctx context.Context) error signatures. Errors are reported with automatic attribution — E2EFixture.BeforeAll failed: connection refused.
A package fixture lives as long as the suite that binds it: set up before the suite's BeforeAll, torn down after its AfterAll, also when a test panics or the run is stopped. Setup that must happen once per run belongs in a shared fixture. A suite sets up only the fixtures it binds and their dependencies, so a fixture it does not use never delays or fails it.
Package fixtures support all four lifecycle hooks. BeforeEach/AfterEach wrap every individual test case, running outside the suite's own hooks:
Nesting
Fixtures compose through named pointer fields — the same pattern suites use to reference fixtures. A fixture can depend on multiple other fixtures. The generator resolves dependencies and wires the lifecycle automatically:
type DBFixture struct { Pool *pgxpool.Pool } func (f *DBFixture) BeforeAll(ctx context.Context) error { // start database container, populate f.Pool } type CacheFixture struct { Client *redis.Client } func (f *CacheFixture) BeforeAll(ctx context.Context) error { // start Redis container, populate f.Client } type APIFixture struct { DB *DBFixture // dependency — wired automatically Cache *CacheFixture // dependency — wired automatically ServerURL string } func (f *APIFixture) BeforeAll(ctx context.Context) error { // start API server using f.DB.Pool and f.Cache.Client } type OrderTestSuite struct { Fixture *APIFixture // accesses DB, Cache, and ServerURL }
Dependencies are set up first. Independent fixtures set up in parallel; teardown runs in reverse:
Shared fixtures
Structs ending in SharedFixture run in a subprocess and share state across packages via JSON serialization. Resources that can't serialize (connection pools, caches) are reconstructed in each test process via Hydrate:
type PostgresSharedFixture struct { ConnStr string // serialized — transfers across processes Pool *pgxpool.Pool // local — reconstructed via Hydrate } func (f *PostgresSharedFixture) BeforeAll(ctx context.Context) error { f.ConnStr = startPostgres(ctx) return f.connect(ctx) } func (f *PostgresSharedFixture) Hydrate(ctx context.Context) error { return f.connect(ctx) } func (f *PostgresSharedFixture) Dehydrate(ctx context.Context) error { f.Pool.Close(); return nil } func (f *PostgresSharedFixture) connect(ctx context.Context) error { var err error f.Pool, err = pgxpool.New(ctx, f.ConnStr) return err }
BeforeAll always sets transfer fields. Local fields only need to be set in BeforeAll when a dependent shared fixture accesses them in the DAG — otherwise they may add an idle resource to the subprocess. Hydrate handles local field reconstruction in test processes.Hydrate are automatically classified as local and excluded from serialization. Everything else transfers. No annotations needed.SharedFixture dependencies
SharedFixtures can depend on other SharedFixtures via pointer fields — the same pattern used by package fixtures. BeforeAll runs in dependency order (parents first, independent fixtures in parallel). Cyclic dependencies are rejected at generation time.
type SchemaSharedFixture struct { Postgres *PostgresSharedFixture // dependency — Postgres starts first Version string } func (f *SchemaSharedFixture) BeforeAll(ctx context.Context) error { // f.Postgres.ConnStr is available return migrate(f.Postgres.ConnStr) }
B.BeforeAll() can access all of A's fields, including local fields like connection pools — provided A.BeforeAll() set them. When a dependent only needs transfer fields, skipping local setup in the parent avoids idle resources in the subprocess.Suites are dispatched as soon as their specific shared fixture dependencies are ready — they don't wait for unrelated fixtures. Transitive dependencies are included automatically: if a suite needs SchemaSharedFixture, it also gets PostgresSharedFixture.
Assertions
Type-safe generics with compile-time checking. Zero external dependencies. Works with *gotest.T, *testing.T, *gotest.B and *gotest.F.
t.Helper() — the failure is located at the line that failed, a helper's included, and the message names the outermost caller from the test file.Equality & identity
| Function | Description |
|---|---|
Equal(t, expected, actual) | Deep equality. Cross-type comparison is a compile error. Failures include a diff. |
NotEqual(t, a, b) | Inverse of Equal. |
Zero(t, value) | Value equals its zero value. |
NotZero(t, value) | Value is not zero. |
Nil(t, obj) | Nil check for non-comparable nilables (slices, maps, funcs). Type-guarded; comparable nilables use Zero. |
NotNil(t, obj) | Inverse of Nil. |
Empty(t, obj) | Slice, map, string, or channel is empty. |
NotEmpty(t, obj) | Not empty. |
Errors
| Function | Description |
|---|---|
NoError(t, err) | Error is nil. |
Error(t, err) | Error is not nil. |
ErrorIs(t, err, target) | Wraps errors.Is. |
ErrorAs[E](t, err) | Wraps errors.As. Returns the matched error. |
ErrorContains(t, err, substr) | Error message contains substring. |
Collections
| Function | Description |
|---|---|
Contains(t, haystack, needle) | String, slice, or map contains value. |
NotContains(t, s, v) | Inverse of Contains. |
Len(t, obj, n) | Collection has exactly n elements. |
ElementsMatch(t, a, b) | Same elements regardless of order. |
Subset(t, list, sub) | All elements of sub exist in list. |
Comparison & numeric
| Function | Description |
|---|---|
Greater(t, a, b) | a > b. Constrained to cmp.Ordered — comparing incomparable types is a compile error. |
GreaterOrEqual(t, a, b) | a ≥ b. |
Less(t, a, b) | a < b. |
LessOrEqual(t, a, b) | a ≤ b. |
InDelta(t, expected, actual, delta) | Difference within delta. Works with any numeric type. |
Strings, time & other
| Function | Description |
|---|---|
Regexp(t, pattern, str) | String matches regex pattern. |
JSONEq(t, expected, actual) | JSON-equal. Accepts string, []byte, io.Reader, or any marshalable value. |
TimeWithin(t, expected, actual, tol) | Times within tolerance. |
TimeIsNow(t, ts, tolerance) | Timestamp is approximately now. |
True(t, v) / False(t, v) | Boolean checks. |
Panics(t, fn) | Function panics. Returns the recovered value. |
Fail(t, msgAndArgs...) | Explicit immediate failure. |
Must(val, ok) | Unwraps (T, error) and (T, bool) pairs. Panics on error or false — for test setup, not assertions. |
Async
| Function | Description |
|---|---|
Eventually(t, timeout, tick, fn) | Poll func(poll *R) until assertions pass or timeout. |
Consistently(t, duration, tick, fn) | Assert func(poll *R) holds for the full duration. |
Go(t, fn) | Run fn on a goroutine that captures panics and re-raises them on the test's goroutine. Returns a wait func; also registered as cleanup. |
poll (a *gotest.R) exactly as you would use t. Failures are collected, not propagated, enabling retry semantics.t.Helper(), so failures report the caller's file and line — not the assertion library internals.Configuration
Sensible defaults. Override only what you need — via project file, marker methods, or CLI flags.
Precedence
There are two independent configuration scopes:
Project scope — CLI flags override the project file. These control the test runner itself:
Code scope — marker methods override built-in presets. These control individual suites and fixtures:
The two scopes are independent — they don't override each other. For shared fixture timeouts specifically, --setup-timeout is an outer wall-clock budget on the entire setup phase, while SharedFixtureConfig().Timeout is an inner per-fixture deadline. Both run concurrently — whichever fires first wins. When --setup-timeout is omitted, the default outer budget of 2 minutes applies; set it to 0 to disable the outer budget and let only the per-fixture timeouts govern.
Project file — .gotest.yml
Place a .gotest.yml in your project root. gotest finds it by walking up from the working directory, stopping at the first match or at a go.mod boundary. CLI flags always take precedence; zero or omitted fields use defaults.
| Field | Type | Default | CLI override | Effect |
|---|---|---|---|---|
tags | string | none | -tags | Build tags, comma-separated (e.g. "integration,e2e"). |
setup-timeout | duration | 2m | --setup-timeout | Total budget for all shared fixture setup. 0 disables. |
timeout | duration | 15m | --timeout | Global pipeline deadline. 0 disables. |
min-coverage | int | 0 (off) | --min | Minimum coverage percentage, 0–100. |
parallel | int | 0 (auto) | --parallel | Total concurrent test method budget. 0 means 2×GOMAXPROCS, auto-halved under -race/-msan/-asan. |
compile-parallel | int | 0 (auto) | --compile-parallel | Concurrent compilation processes. 0 means NumCPU, auto-halved under -race/-msan/-asan. |
debounce | duration | 200ms | --debounce | Watch mode re-run delay. |
lint.skip | [string] | none | — | Non-integrity lint rules to disable: assertion-simplify, assertion-redundant, fail-guard, t-escape, stdlib-test, testify. |
tags: integration
setup-timeout: 2m
min-coverage: 80
parallel: 12
debounce: 500ms
lint:
skip:
- testifySuite configuration
| Field | Default | Effect |
|---|---|---|
Timeout | 30s | Per-test-case deadline. |
SetupTimeout | 30s | BeforeAll / AfterAll deadline. |
FailFast | false | Stop the suite on first failure. |
Parallel | false | Run test methods concurrently. |
Exclusive | false | Schedule the suite's process strictly alone — after every non-exclusive suite has finished, one exclusive suite at a time. For wall-clock-sensitive or resource-contended suites. |
Fixture configuration
| Field | Default | Effect |
|---|---|---|
Timeout | 2 min | BeforeAll / AfterAll deadline. |
Retries | 0 | BeforeAll retry attempts. |
RetryDelay | 0 | Pause between retries. |
Presets
Start with a preset, override individual fields:
| Preset | Timeout | SetupTimeout | Retries | Use case |
|---|---|---|---|---|
DefaultSuiteConfig() | 30s | 30s | 0 | Unit and integration tests |
IntegrationSuiteConfig() | 2 min | 5 min | 0 | Heavier integration tests |
DefaultFixtureConfig() | 2 min | — | 0 | Standard fixtures |
ContainerFixtureConfig() | 5 min | — | 1 | Testcontainers, image pulls |
gotest.NoDeadline disables a deadline. Booleans, Retries and RetryDelay are used as written, so gotest.SuiteConfig{Parallel: true} runs in parallel with the 30s defaults.Coverage
Statement-weighted coverage from the Go coverage profile. One source of truth, no heuristics.
How it's calculated
The Go compiler instruments code into basic blocks. Each block records how many statements it contains and how many times it was executed. Coverage at any scope — file, directory, workspace — is:
A directory's percentage is the weighted sum of its children, weighted by statement count. This means a parent's number is always derivable from its children — no rounding surprises, no averaging artifacts.
Merged and cross-package profiles
Merged profiles — concatenated per-suite profiles, or overlapping -coverpkg runs — can repeat the same block. Each unique file and range is counted once, keeping the maximum execution count, matching go tool cover behavior. -coverpkg itself is a standard go test flag and passes through unchanged.
numStatements as the sole metric. No line counting, no token scanning, no filesystem heuristics. The profile is the source of truth.Benchmarking
Benchmarks are suite methods. A baseline is a file. A gate is a number.
Writing benchmarks
A Benchmark-prefixed method on any suite takes a *gotest.B. The suite's fixtures and lifecycle hooks apply as they do to tests, so setup happens where it always does — outside the measured loop.
func (s *CacheTestSuite) BenchmarkGetHit(b *gotest.B) { key := s.Corpus.Keys[0] // read fixture state above the loop for b.Loop() { s.cache.Get(key) } }
b.Loop() times the fixture, not the code under test. The bench-fixture-io lint rule flags it.Running them
| Command | Effect |
|---|---|
gotest bench ./... | Run every suite's generated Benchmark<SuiteName> wrapper, one suite at a time. |
gotest bench --spec ./... | Render the results as a spec tree instead of raw benchmark lines. |
-bench='^BenchmarkCacheTestSuite$/^BenchmarkGetHit$' | Scope to one method: the wrapper runs each method under b.Run with its own name. |
-benchtime=100x, -count=3 | Standard go test flags, forwarded unchanged. |
-test.benchmem is on by default, so every line carries B/op and allocs/op.Baselines, deltas, and the gate
| Flag | Effect |
|---|---|
--save=<path> | Write this run's results as a JSON baseline. Bare --save= uses bench.baseline from .gotest.yml. |
--against=<path> | Compare this run to a saved baseline and print a delta table. Defaults to bench.baseline. |
--gate=<pct> | Exit 1 when the worst significant regression exceeds pct percent. Needs a comparison; defaults to bench.gate, where 0 disables. |
--json | One versioned JSON document: the run's results, every delta when a comparison ran, and the gate verdict when a gate is active. For tooling, including the editor. |
In .gotest.yml, bench.baseline and bench.gate set the defaults, so CI and a laptop agree on what a regression is without repeating flags.
goos, goarch and Go version that produced it. Compare against one recorded elsewhere and --against says so on stderr — WARN: baseline was recorded in a different environment, naming each field — while --json carries the same under envMismatch. The comparison still runs and the exit code is unchanged: across machine classes the deltas measure the machines too, and only you know whether two runners are alike enough for that to matter.Fuzzing
Fuzz targets are suite methods, and their inputs are Go types — including structs the engine cannot take.
Writing a fuzz target
A Fuzz-prefixed method takes a *gotest.F. Seeds go in with f.Add, the property goes in f.Fuzz, and the generator emits one Fuzz<SuiteName>_<MethodName> wrapper per method.
func (s *FrameCodecTestSuite) FuzzFrameRoundTrip(f *gotest.F) { f.Add(Frame{Version: 1, Topic: "orders.created", Payload: []byte(`{"id":1}`)}) f.Fuzz(func(t *gotest.T, in Frame) { out, err := Decode(Encode(in)) gotest.NoError(t, err) gotest.Equal(t, in, out) // property: round trip }) }
Struct inputs
Go's fuzzing engine accepts fifteen types, none of them a struct. gotest fans a struct into one engine argument per leaf field at generation time and reassembles the value before each execution. Two consequences follow: a type the fan cannot express is refused when you generate, not hours into a session, and a seed stays an ordinary Go literal instead of a byte blob.
Sessions
| Flag | Effect |
|---|---|
--for=<dur> | Wall-clock budget for the whole session (default 1m), split across targets — never less than 10s each. --for=0 fuzzes until interrupted. |
--jobs=<n> | Concurrent targets (default max(1, GOMAXPROCS/2)). Each target is its own go test -fuzz process. |
--target=<Fuzz...> | Fuzz one target by wrapper name; an unmatched name lists what exists. |
--no-harvest | Skip mining table-test literals into seeds for this run (fuzz.harvest in .gotest.yml). |
Exit codes: 0 when the budget ran out with nothing found, 1 for a finding — a failing target or a new crasher file — and 2 when the session could not run as asked.
f.Add seed and every corpus entry under testdata/fuzz/ runs as an ordinary subtest of a plain gotest run. gotest fuzz is how you spend time looking for new inputs.Crashers
| Command | Effect |
|---|---|
gotest fuzz triage ./... | Re-run every corpus entry, printing its decoded input and the failure cause, or status: no longer failing. Exits 1 while any crasher still fails. |
gotest fuzz promote ./... | Splice each crasher into its method as a permanent f.Add(...) seed, after the last existing seed, and delete the corpus file. |
A crasher whose method cannot be located with confidence is left in place with a warning; promote never partially edits source.
CLI Reference
One command, multiple modes. All go test flags pass through unchanged.
Commands
| Command | Effect |
|---|---|
gotest ./... | Generate overlays and run tests. The default workflow. |
gotest watch ./... | Re-run on file changes. Only affected packages re-run. |
gotest spec ./... | Run tests and render the behavioral specification. With --static, render it from source without running. |
gotest discover ./... | Discover test suites and output JSON (feeds editor integrations). |
gotest summary ./... | Failure-focused summary for CI. Shows only failing tests with assertion output. |
gotest bench ./... | Run Benchmark* suite methods serially; --save, --against and --gate keep and compare baselines. |
gotest fuzz ./... | Search for new failing inputs with Fuzz* suite methods for a time budget (--for); fuzz triage and fuzz promote handle what it finds. |
gotest lint ./... | Static analysis for test suites — see Linter. |
gotest scaffold ./pkg/user.Svc | Generate a suite skeleton from any Go type. |
gotest migrate ./... | Convert testify/suite tests automatically. |
gotest generate ./... | Generate suite files without running tests. |
gotest clean ./... | Remove orphaned generated files. |
gotest refactor toggle-focus <file> <id> | Toggle F_/X_ prefixes programmatically. |
gotest skill install | Install the coding-agent skill shipped with this version (v1.34+); --dir picks the skills directory. |
gotest prepare ./... | Start shared fixtures for debugging; blocks until SIGTERM. |
gotest version | Print version information. |
gotest help [subcommand] | General or subcommand-specific help. |
Flags
| Flag | Effect |
|---|---|
--ci | CI mode: fail on focus prefixes, snapshot read-only. Auto-detected from CI env var; opt out with GOTEST_CI=0. |
--spec | Render the spec view in place of the default output. |
--update-snapshots | Regenerate all snapshot files. |
--no-cache | Disable overlay cache, force fresh generation. |
--min=<pct> | Fail if coverage falls below threshold. |
--format=<fmt> | Output format for spec command: md (Markdown) or json (structured JSON). |
--output=<path> | Write formatted output to file. |
--no-color | Strip ANSI codes from output. |
--debug | Preserve overlays after the run for inspection. |
--setup-timeout=<dur> | Total budget for all shared fixture setup (default: 2m, 0 to disable). |
--timeout=<dur> | Global pipeline deadline (default: 15m, 0 to disable). |
--parallel=<n> | Total concurrent test method budget (default: 2×GOMAXPROCS, auto-halved for -race/-msan/-asan). |
--compile-parallel=<n> | Concurrent compilation processes (default: NumCPU, auto-halved for -race/-msan/-asan). |
--save=<path> | Save a benchmark run as a JSON baseline (bench). |
--against=<path> | Compare a benchmark run against a saved baseline and print the delta table (bench). |
--gate=<pct> | Fail if the worst significant benchmark regression exceeds the threshold (bench). |
--json | Emit one versioned JSON report instead of human output (bench). |
--for=<dur> | Approximate wall-clock budget for the whole fuzz session, split across targets (fuzz; default 1m, 0 removes the budget). |
--jobs=<n> | Maximum concurrent fuzz targets (fuzz; default max(1, GOMAXPROCS/2)). |
--target=<name> | Fuzz exactly one generated target by its wrapper name (fuzz). |
--no-harvest | Do not mine table-test literals into fuzz seeds for this run. |
--static | Read the specification from source without running the suites; nodes carry no verdict (spec command). |
--render-only | With --input, exit 0 on a failing or incomplete stream: report whether rendering succeeded, not the test verdict. |
--debounce=<dur> | Watch mode debounce interval (default 200ms). |
--input=<path> | Read test output from file instead of running tests (spec, summary). Exits 2 when the input holds no test events, a package or failed build never reported its verdict, or a package failed to build. |
--github | GitHub CI mode: emit ::error annotations and write step summary (summary command). Auto-detected via $GITHUB_ACTIONS. |
--coverage=<file> | Include coverage from profile in summary output (summary command). |
--badge=<file> | Write a self-contained coverage badge SVG; enables -coverprofile when nothing else does (summary command). |
go test flags work unchanged: -v, -race, -cover, -count, -run, -skip, -json, -short, -timeout.
Every matched package ends in a verdict, as with go test, printed in package order: one gotest runs nothing in reports [no test files], [no suites] (stdlib tests only, standard library packages too) or 0.000s [no tests to run] (benchmarks only, every suite excluded with X_, or -run deselected every suite), in text and in -json, where each package opens with one start event as in go test -json.
Patterns that match no package at all verified nothing: every run mode exits 2 with FAIL: "<pattern>" matched no packages on stderr. A pattern that matches nothing beside others that did only warns, as with go test. generate and discover, which verify nothing, only warn and exit 0. An empty pattern ("") is the current directory, as with go test. gotest takes packages, not files: a .go file argument exits 2 and names it.Linter
Twenty-eight rules in three tiers. A rule's tier determines how it may be suppressed.
Integrity rules
Violations can make test outcomes unreliable or leak resources. These rules can only be suppressed per line — never project-wide.
| Rule | Detects |
|---|---|
focus | Committed F_ prefixes on suites or methods. |
receiver | Suite methods with value receivers instead of pointer receivers. |
lifecycle-typo | Method names one typo away from a lifecycle hook (BeforAll, AfterEeach). |
lifecycle-pair | BeforeAll without AfterAll — resources may leak. |
x-lifecycle | X_ prefix on a lifecycle hook — a no-op. |
test-signature | Test methods not accepting *gotest.T (or *testing.T). |
suite-lifecycle | Cleanup/Parallel/Run via t.T() — they bypass the suite lifecycle. Run is flagged in any function, since its subtests drop the suite's deadline. |
poll-scope | Assertions inside Eventually/Consistently callbacks using the outer t instead of poll. |
assertion-type-guard | Nil/Empty on types their runtime guards would reject. |
shared-fixture-undeclared | Suite reads a shared fixture it never declared — window scheduling only starts declared fixtures, so the read may hit one that never started or is already released. |
generated-file | Generated overlay files present in source control. |
bench-loop | Benchmark* methods that never touch b.Loop()/b.N — nothing iterates, so the numbers lie. |
fuzz-determinism | Fuzz targets reading time.Now, math/rand or os.Getenv — corpus replay and coverage guidance degrade. |
fuzz-struct-corpus | On-disk corpus entries for a struct-typed fuzz target — bound to the field order; gotest fuzz promote turns them into typed seeds. |
Expressiveness rules
The test is correct but its syntax can be improved. These rules can be suppressed per line or project-wide via lint.skip; -fix applies the safe rewrites in one run, overlapping ones included, then reports what it could not fix (exit 3 while findings remain); -diff prints exactly what -fix would write, as a unified diff with paths relative to the current directory, and writes nothing — gotest lint -diff ./... | git apply applies it (a file outside that directory needs git apply --unsafe-paths; a symlink is named by the file it leads to, which -fix writes).
| Rule | Detects |
|---|---|
assertion-simplify | Simplifiable assertions: True(t, a == b) → Equal, Len(t, x, 0) → Empty, … |
assertion-redundant | An assertion made redundant by the next one on the same argument. |
fail-guard | if cond { Fail/Fatal(...) } guards — the assertion expresses the check directly. |
t-escape | Unnecessary t.T() convenience escapes (Errorf, Helper, Log, Fatal, …), and t.T().Context()/t.T().Deadline(), which drop the suite's Timeout — also inside *testing.T helpers handed t.T(). |
config-no-deadline | A literal negative duration on SuiteConfig.Timeout/SetupTimeout or FixtureConfig.Timeout — every negative disables the deadline; the fix spells it gotest.NoDeadline. |
behavior-wording | When("when …") or It("it …") — the spec renders the connective and the ✓ glyph plays "it"; the fix drops the word. |
bench-fixture-io | Fixture-backed reads inside the measured loop — times the fixture, not the code. |
bench-wait | time.Sleep/Eventually/Consistently inside the measured loop — times the wait. |
fuzz-no-oracle | Fuzz callbacks that assert nothing — only panics are caught. |
fuzz-seed | Fuzz targets with no f.Add seed — exploration starts blind. |
fuzz-hook-io | IO in BeforeEach/AfterEach of a fuzzing suite — the hooks replay around every execution. |
fuzz-raw-seed | Raw []byte seeds on a position that does not take []byte — write a typed literal. |
Migration rules
Adoption aids for codebases moving to gotest — coexistence is legitimate. These rules can be suppressed per line or project-wide via lint.skip.
| Rule | Detects |
|---|---|
stdlib-test | func TestX(*testing.T) — consider a suite method; coexisting stdlib tests are legitimate. |
testify | Any github.com/stretchr/testify/* import — migration incomplete. |
Suppression
Silence a single diagnostic with //nolint:<rule> on its line or in a comment block of its own ending on the line directly above it; on the package line it applies file-wide. Non-integrity rules can also be disabled per run (-skip-<rule>) or project-wide (.gotest.yml → lint.skip).
Exit codes
| Code | Meaning |
|---|---|
0 | No findings (with -fix or -diff: none left after fixing). |
1 | Nothing analyzable: one or more target packages do not compile — they could not be analyzed, so the run fails instead of passing silently; patterns that match no package, or name a directory or package that does not exist (a pattern matching nothing beside others that do only warns on stderr, as go does); fixed sources that no longer compile; or fixed files that could not be written (-fix writes them all or none; on a failure stderr names any that were). |
2 | Usage or configuration error (e.g. an integrity rule in lint.skip). |
3 | Findings reported (with -fix or -diff: the findings left after fixing). |
130 | -fix or -diff stopped by a signal; nothing was written (once the fixed files are being written, they all are first). |
go/analysis analyzer, built into the CLI as gotest lint. There is no golangci-lint plugin — run it as its own CI step alongside your existing linter. In GitHub Actions, gotest lint auto-detects the runner (GITHUB_ACTIONS=true, or explicitly via --github) and additionally emits inline PR annotations per finding plus a findings table in the step summary; exit codes and suppression are unchanged.CI Integration
Failure-focused summaries, inline PR annotations, and coverage reports — out of the box.
gotest summary
The summary subcommand shows only failing tests with their assertion output, filtering out === RUN, === CONT, --- PASS noise. When all tests pass, it prints a single success line:
147 tests passed (2.3s) Coverage: 82.4%
3 of 147 tests failed
FAIL pkg/foo TestValidateInput / empty string (12ms)
foo_test.go:42: expected error, got nil
FAIL pkg/bar TestProcessOrder / concurrent writes (1.2s)
bar_test.go:88:
expected: []string{"a", "b", "c"}
got: []string{"a", "c", "b"}In GitHub Actions, --github (auto-detected via $GITHUB_ACTIONS) emits ::error annotations that appear inline on PR diffs and writes a markdown summary to the job summary panel.
GitHub Action
The repository includes a composite action at mvrahden/go-test@v1 that wraps gotest summary --github:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- uses: mvrahden/go-test@v1
with:
packages: ./...
race: true
coverage: true
min-coverage: 80Action inputs
| Input | Default | Description |
|---|---|---|
packages | ./... | Package patterns to test. |
race | false | Enable the race detector. |
coverage | false | Enable coverage profiling and reporting. |
min-coverage | none | Minimum coverage percentage (0–100). Fails the step if below. |
flags | none | Additional gotest flags (--double-dash style). |
go-test-flags | none | Additional go test flags (-single-dash style). |
badge | false | Render a coverage badge and publish it as coverage.svg to badge-branch, from the default branch only. Turns coverage on; needs contents: write. |
badge-branch | ci/badges | Branch the badge is published to; created automatically on first use. |
bench | false | Run benchmarks after the tests (gotest bench --spec --json). |
bench-baseline | none | Baseline JSON file to compare benchmarks against (--against). |
bench-gate | none | Fail the run if a benchmark regresses by more than this percent (--gate). |
bench-save | false | Save the benchmark run as a JSON baseline at this path (--save); an empty string saves to bench.baseline from .gotest.yml. |
fuzz | false | Run a budgeted fuzz session after the tests (gotest fuzz --for); a new crasher fails the step. |
fuzz-for | none | Approximate wall-clock budget for the fuzz session; empty uses the CLI's default of one minute. |
fuzz-cache | true | Carry Go's fuzz cache between runs, so each session resumes where the last one stopped. |
version | gomod | Version to install, or gomod to resolve from go.mod. |
Action outputs
| Output | Description |
|---|---|
exit-code | Test process exit code. |
coverage | Coverage percentage (empty if coverage not enabled). |
badge | Path of the rendered coverage badge SVG (empty unless badge is enabled and the tests ran). |
bench-report | Path to the gotest bench --json report file (empty if bench is not enabled). |
bench-breached-keys | Comma-joined benchmark keys that breached the gate (empty if none or no gate). |
fuzz-crashers | Comma-joined workspace-relative paths of the corpus files the fuzz session added (empty if none or fuzz is not enabled). |
Version resolution
By default (version: gomod), the action runs the gotest your project's go.mod selects: go tool when the tool is declared there, go run otherwise. This keeps the CLI version in sync with the library you already depend on — no version drift between CI and local development.
Set version: latest or a specific tag (e.g. v1.2.0) to install a standalone binary via go install instead. This is useful when your project doesn't depend on go-test as a library.
gomod default, ensure gotest is in your go.mod: declare it as a tool with go get -tool github.com/mvrahden/go-test/cmd/gotest@<version>, naming a release whose go line your module takes (a bare or @latest go get can raise your module's go line).Manual CI setup
For non-GitHub CI systems, run gotest summary directly:
gotest summary ./... -race -coverprofile=coverage.out
Or pipe from go test -json to summarize existing output without re-running tests:
go test -json ./... | gotest summary --input=-