Contents
  1. Conceptual Model
  2. Naming Conventions
  3. Suite Lifecycle
  4. Test DSL
  5. Fixtures
  6. Assertions
  7. Configuration
  8. Coverage
  9. CLI Reference

Conceptual Model

Test suites are behavioral specifications. Every level of the hierarchy maps to a concept.

Go constructSpec conceptExample
structSubjectUserServiceTestSuite
methodCapabilityTestCreate
When()Context"when email is valid"
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:

UserService Create when email is valid creates the user sends a welcome email when email already exists returns ErrDuplicate Delete soft-deletes the user

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

PatternMeaning
*TestSuiteTest suite. Each generates a func Test* entry point.
*FixturePackage fixture. Shared setup for all suites in a package.
*SharedFixtureCross-package fixture. Runs in a subprocess, state transfers via JSON.

Methods

MethodMeaning
Test*Test case. Becomes a subtest.
BeforeAllRuns once before all tests in the suite.
AfterAllRuns once after all tests. Registered as cleanup, so it always runs.
BeforeEachRuns before every test case.
AfterEachRuns 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.
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 / DehydrateReconstruct / clean up local resources in shared fixtures after state transfer.

Prefixes

PrefixEffectUse case
F_Focus — only focused items runTight iteration during development
X_Exclude — always skippedTemporarily 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.

Suite Lifecycle

Predictable execution order with guaranteed cleanup — even on panics and fatal errors.

Execution order

BeforeAll ├── BeforeEach Test A AfterEach (deferred) ├── BeforeEach Test B AfterEach (deferred) ├── BeforeEach Test C AfterEach (deferred) AfterAll (via t.Cleanup — always runs)

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}. Requires a returning BeforeEach so each parallel test gets its own state.

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 valid", 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))
}

Callback API — same semantics, different style:

t.Each(cases, func(it *gotest.T, tc Case) {
    gotest.Equal(it, tc.Want, parse(tc.Input))
})
Each entry becomes a subtest. Uses the 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:

t.MatchSnapshot(render(input))              // auto-named from test path
t.MatchSnapshot(render(other), "variant")  // explicit snapshot name

On first run, the snapshot is created. On subsequent runs, the output is compared and failures include a diff. Update all snapshots with gotest --update-snapshots ./....

Async polling

Two forms — a simple boolean check, or rich assertion polling with the full assertion library:

Boolean polling (function)
gotest.Eventually(t, func() bool {
    return s.svc.IsReady()
}, 5*time.Second, 100*time.Millisecond)
Rich assertion polling (method on *T)
t.Eventually(5*time.Second, 100*time.Millisecond, func(poll *gotest.T) {
    result, err := s.store.Get("key")
    gotest.NoError(poll, err)
    gotest.Equal(poll, "completed", result.Status)
})

The method form collects intermediate failures without propagating them. On timeout, the last poll's assertion failures are reported. Consistently works the same way — it asserts a condition holds for the full duration and fails on the first violation.

Helpers

MethodDescription
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, shared across suites.

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.

Package fixtures support all four lifecycle hooks. BeforeEach/AfterEach wrap every individual test case, running outside the suite's own hooks:

Fixture.BeforeEach └── Suite.BeforeEach Test Suite.AfterEach Fixture.AfterEach

Nesting

Fixtures compose through named pointer fields — the same pattern suites use to reference fixtures. A child fixture points to its parent, and the generator wires the lifecycle automatically:

type InfraFixture struct {
    Pool *pgxpool.Pool
}

func (f *InfraFixture) BeforeAll(ctx context.Context) error {
    // start database container, populate f.Pool
}

type APIFixture struct {
    Infra     *InfraFixture // named field — wired automatically
    ServerURL string
}

func (f *APIFixture) BeforeAll(ctx context.Context) error {
    // start API server using f.Infra.Pool
}

type OrderTestSuite struct {
    Fixture *APIFixture // accesses f.Infra.Pool and f.ServerURL
}

The parent's hooks run first, wrapping the child's. The suite sees the fully initialized fixture chain:

InfraFixture.BeforeAll └── APIFixture.BeforeAll └── Suite.BeforeAll ├── Suite.BeforeEach Test Suite.AfterEach └── Suite.AfterAll └── APIFixture.AfterAll InfraFixture.AfterAll

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) Hydrate(ctx context.Context) error {
    var err error
    f.Pool, err = pgxpool.New(ctx, f.ConnStr)
    return err
}
Fields assigned in Hydrate are automatically classified as local and excluded from serialization. Everything else transfers. No annotations needed.

Assertions

Type-safe generics with compile-time checking. Zero external dependencies. Works with both *gotest.T and *testing.T.

Equality & identity

FunctionDescription
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.
Empty(t, obj)Slice, map, string, or channel is empty.
NotEmpty(t, obj)Not empty.

Errors

FunctionDescription
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

FunctionDescription
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

FunctionDescription
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

FunctionDescription
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

FunctionDescription
Eventually(t, fn, timeout, tick)Poll func() bool until true or timeout.
Consistently(t, fn, duration, tick)Assert func() bool holds for the full duration.
These are the standalone boolean-polling forms. For rich assertion polling with func(poll *T), use the method forms on *T.
All assertions call 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 marker methods.

Suite configuration

FieldDefaultEffect
Timeout30sPer-test-case deadline.
SetupTimeout30sBeforeAll / AfterAll deadline.
Retries0Per-test-case retry attempts on failure.
FailFastfalseStop the suite on first failure.
ParallelfalseRun test methods concurrently.

Fixture configuration

FieldDefaultEffect
Timeout2 minBeforeAll / AfterAll deadline.
Retries0BeforeAll retry attempts.
RetryDelay0Pause between retries.

Presets

Start with a preset, override individual fields:

PresetTimeoutSetupTimeoutRetriesUse case
DefaultSuiteConfig()30s30s0Unit and integration tests
IntegrationSuiteConfig()2 min5 min0Heavier integration tests
DefaultFixtureConfig()2 min0Standard fixtures
ContainerFixtureConfig()5 min1Testcontainers, image pulls
Use a negative duration (Timeout: -1) to explicitly disable a timeout. Zero means "keep the default."

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:

covered = statements in blocks executed at least once total = all instrumented statements percentage = covered / total

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.

Breadth indicator

Coverage percentage answers: "How well-tested is the code my tests reach?"

The breadth indicator answers a complementary question: "How much of my codebase do my tests reach at all?" It shows profiled source files vs. total source files per directory. A file at 0% still counts as reached — it was instrumented.

Cross-package coverage

Test-only packages (no production code) run with -coverpkg=./..., which instruments the entire module. These cross-package profiles are supplementary — they can increase coverage for files already in scope, but don't expand the file scope. This prevents integration test packages from inflating coverage numbers for code they touch incidentally.

Coverage uses the profile's numStatements as the sole metric. No line counting, no token scanning, no filesystem heuristics. The profile is the source of truth.

CLI Reference

One command, multiple modes. All go test flags pass through unchanged.

Commands

CommandEffect
gotest ./...Generate, test, cleanup. The default workflow.
gotest watch ./...Re-run on file changes. Only affected packages re-run.
gotest spec ./...Run tests and render the behavioral specification.
gotest lint ./...Static analysis for test suites.
gotest scaffold ./pkg/user.SvcGenerate 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 generated files.
gotest refactor toggle-focus <file> <id>Toggle F_/X_ prefixes programmatically.

Flags

FlagEffect
--ciFail the build if any focus prefix is committed.
--specAppend spec summary after normal test output.
--update-snapshotsRegenerate all snapshot files.
--min=<pct>Fail if coverage falls below threshold.
--format=mdMarkdown output for spec command.
--output=<path>Write formatted output to file.
--no-colorStrip ANSI codes from output.
--debugKeep generated files after the run.
--setup-timeout=<dur>Shared fixture setup deadline (default 1m).
--debounce=<dur>Watch mode debounce interval (default 200ms).
--input=<path>Read test output from file instead of running tests (spec command).
Standard go test flags work unchanged: -v, -race, -cover, -count, -run, -json, -short, -timeout.