golang-testing
Skill1 starsChanged 6 months agoMITView on GitHub
Write idiomatic Go tests — unit tests, table-driven patterns, mocking, and testcontainers-go integration tests against real Postgres, Redis, and other containerised dependencies.
We split this skill into the rules it gives an agent
97 rules. 0 are common, 64 are distinctive, and 33 are very specific to this project.
0
Common
Most of the 43 edited versions we read keep these.
64
Distinctive
This author's own choices. Worth a look.
33
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 10
Use this skill when writing, reviewing, or refactoring Go tests — unit tests, table-driven tests, or container-backed integration tests.
- Very specific: line 25
Use black-box tests with an external test package: package mypkg_test
- Distinctive: line 26
Test public behaviour and interfaces, not internal implementation details
- Very specific: line 27
Use one top-level test function per public function/method/behaviour, with subtests for scenarios
- Very specific: line 28
Call t.Parallel() by default in top-level tests and subtests unless shared state, process-wide mutation, timing, or external resources make …
- Very specific: line 29
Use t.Run subtests by default, even when not using table-driven tests
- Distinctive: line 30
Use table-driven tests only when there is a finite, related set of input/output scenarios that benefits from shared structure
- Very specific: line 31
Use testify/require for preconditions and fatal assertions, and testify/assert for non-fatal assertions
- Very specific: line 32
Use testify/mock only for true external-system boundaries or very small/simple seams.
- Very specific: line 33
Use real containers via testcontainers-go for integration tests that need real Postgres, Redis, or similar infrastructure, rather than …
- Distinctive: line 34
Keep tests deterministic, hermetic, and readable
- Distinctive: line 39
Fast feedback: - Keep most tests cheap enough to run constantly
- Distinctive: line 40
Deterministic: - Avoid sleeps, real clocks, network access, and ambient environment dependence
- Distinctive: line 41
Minimal setup: - Prefer small helpers over deep fixture stacks
- Very specific: line 69
Use same-package tests only when you intentionally need access to unexported helpers and there is no better public seam.
- Distinctive: line 74
Keep helper functions near the tests that use them unless broadly reused
- Very specific: line 75
Prefer _test.go files named after the subject under test, such as service_test.go, handler_test.go, client_test.go
- Distinctive: line 76
Avoid giant catch-all files once a package grows
- Very specific: line 84
Use t.Run even without a table when the scenarios are clearer as explicitly written examples.
- Very specific: line 153
Call t.Parallel() unless there is a concrete reason not to.
- Distinctive: line 163
Avoid or carefully isolate parallelism when tests:
- Distinctive: line 168
Use fixed ports, shared files, or shared databases without isolation
- Distinctive: line 205
Prefer Testify for clearer assertions.
- Very specific: line 218
Use require when the test cannot continue after failure
- Very specific: line 219
Use assert when you want multiple related checks in one scenario
- Distinctive: line 233
Prefer semantic assertions when available:
- Very specific: line 245
Prefer asserting exported sentinel/type behaviour with ErrorIs / ErrorAs
- Distinctive: line 246
Avoid brittle full-string equality unless the exact message is part of the contract
- Distinctive: line 271
Add a test for every bug fix before or with the fix
- Distinctive: line 273
Prefer focused unit tests for combinatorial logic and a smaller number of integration tests for end-to-end confidence
- Distinctive: line 274
If a branch matters to production behaviour, it deserves an assertion
- Distinctive: line 275
Remove duplicate tests that do not add behavioural signal
- Distinctive: line 289
Use the lightest test that proves the behaviour.
- Distinctive: line 304
When an integration test gives stronger confidence with similar complexity, prefer it over elaborate mocking.
- Very specific: line 308
Use testcontainers-go when an integration test needs a real Postgres, Redis, Kafka, NATS, or other containerised dependency instead of a …
- Distinctive: line 314
Use real containers for integration tests, not mocks
- Very specific: line 315
Prefer module-specific helpers like postgres.Run and redis.Run over lower-level generic setup when a module exists
- Very specific: line 316
Prefer testcontainers.Run over older GenericContainer patterns for generic services
- Distinctive: line 319
Keep tests parallel by isolating state inside the shared container per test
- Distinctive: line 320
Use per-test transactions, schemas, databases, logical Redis DBs, or key prefixes instead of sharing mutable state
- Distinctive: line 323
Avoid cross-test and cross-package reuse by default.
- Distinctive: line 334
If the test is meant to validate SQL, migrations, transactions, Redis commands, TTL behaviour, or networked dependency behaviour, a real …
- Very specific: line 339
Prefer modern Run(...) APIs
- Very specific: line 369
If most tests in the package need the dependency, TestMain is a good fit.
- Distinctive: line 373
Use generic startup when there is no higher-level module or when you need custom behaviour.
- Distinctive: line 412
Use this pattern for services without a dedicated module, or when you need full control over files, env vars, commands, or custom wait …
- Distinctive: line 416
Prefer the Postgres module for Postgres integration tests.
- Distinctive: line 463
Prefer the Redis module for Redis integration tests.
- Very specific: line 498
Use module options like WithConfigFile(...), WithTLS(), or WithLogLevel(...) when relevant
- Distinctive: line 504
When most tests in a package need the same dependency, prefer a package-scoped container.
- Very specific: line 564
If tests run with t.Parallel(), they must not step on the same data.
- Distinctive: line 566
Prefer one of these per-test isolation patterns.
- Distinctive: line 572
Per-test transaction + rollback: when your code can run inside an injected transaction
- Distinctive: line 573
Per-test schema: when your app can point each test at its own schema/search path
- Distinctive: line 575
Per-test container: only when the test truly needs process-level or cluster-level isolation
- Distinctive: line 606
do not call it concurrently from parallel tests that share the same Postgres container/database
- Distinctive: line 613
Per-test logical DB: when using standalone Redis and your client can select DB numbers
- Distinctive: line 614
Per-test key prefix/namespace: when logical DB separation is not practical
- Distinctive: line 615
Per-test container: when you need hard isolation or destructive global operations
- Very specific: line 632
Avoid FLUSHALL in tests
- Distinctive: line 639
Delete only that namespace in cleanup
- Very specific: line 651
Start one Postgres container in TestMain
- Distinctive: line 655
Prefer this over one-container-per-test when:
- Distinctive: line 664
Start one Redis container for the package
- Distinctive: line 666
Never use global destructive cleanup across all tests
- Distinctive: line 670
Do not assume container start means service readiness.
- Distinctive: line 683
Prefer runtime discovery over fixed ports:
- Very specific: line 689
Do not hardcode localhost:5432 or localhost:6379.
- Very specific: line 705
use testcontainers.TerminateContainer(...) explicitly after m.Run()
- Distinctive: line 713
Avoid making reuse your default because it:
- Distinctive: line 719
Prefer clean startup/teardown unless you have a deliberate local-only optimisation strategy.
- Distinctive: line 841
Keep expectations minimal and behaviour-focused
- Distinctive: line 842
Avoid over-asserting call order unless it is part of the contract
- Distinctive: line 843
Do not share mock instances across parallel tests
- Distinctive: line 847
Prefer standard-library-friendly seams and hermetic storage.
- Very specific: line 852
Use fstest.MapFS for small read scenarios
- Very specific: line 853
Use t.TempDir() for realistic file creation/update flows
- Distinctive: line 854
Keep filesystem access behind a small adapter if the production code is otherwise hard to isolate
- Very specific: line 890
Avoid mocking os calls directly when a temp dir or fs.FS seam would be simpler.
- Distinctive: line 894
Prefer real HTTP semantics without real network dependencies.
- Very specific: line 898
Use httptest.Server to test client behaviour against realistic responses
- Very specific: line 937
Use this when you only need to simulate one request/response pair and httptest.Server would be unnecessary ceremony.
- Distinctive: line 941
Do not depend on real time in tests.
- Distinctive: line 947
Use fixed timestamps in tests
- Very specific: line 948
Avoid time.Sleep as a synchronisation mechanism
- Distinctive: line 949
Prefer contexts, channels, and explicit signals over waiting for elapsed time
- Very specific: line 975
If production code currently calls time.Now() directly in many places, first introduce a small seam at the package boundary rather than …
- Very specific: line 979
Prefer plain t.Run subtests first without a table when scenarios are clearer as explicitly written examples
- Distinctive: line 980
Use tables when the setup is mostly the same and only inputs/expectations vary
- Distinctive: line 982
Keep test case structs focused.
- Distinctive: line 999
Prefer helper functions that build valid defaults and allow explicit overrides
- Distinctive: line 1002
Keep helpers local to the package unless broad reuse justifies promotion
- Distinctive: line 1015
Avoid these unless there is a strong reason:
- Distinctive: line 1044
When reviewing Go tests, check for:
- Distinctive: line 1054
No hidden global state or cross-test coupling
- Distinctive: line 1064
No fixed host ports
- Distinctive: line 1068
No global destructive cleanup that breaks parallel runs
“Common” is measured: kept by at least 6 in 10 of the 43 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Golang Testing
Use this skill when writing, reviewing, or refactoring Go tests — unit tests, table-driven tests, or container-backed integration tests.
When to Use
- Writing new unit, integration, or package-level tests for Go code
- Refactoring tests toward idiomatic Go structure and stronger signal
- Reviewing Go test suites for maintainability, determinism, and coverage gaps
- Designing fakes, mocks, or test seams around external dependencies
- Testing against a real Postgres, Redis, Kafka, NATS, or other containerised dependency with
testcontainers-go - Verifying SQL, migrations, indexes, transactions, locking, or driver behaviour against real infrastructure
Default Testing Stance
Unless there is a clear reason not to, prefer these defaults:
- Use black-box tests with an external test package:
package mypkg_test - Test public behaviour and interfaces, not internal implementation details
- Use one top-level test function per public function/method/behaviour, with subtests for scenarios
- Call
t.Parallel()by default in top-level tests and subtests unless shared state, process-wide mutation, timing, or external resources make it unsafe - Use
t.Runsubtests by default, even when not using table-driven tests - Use table-driven tests only when there is a finite, related set of input/output scenarios that benefits from shared structure
- Use
testify/requirefor preconditions and fatal assertions, andtestify/assertfor non-fatal assertions - Use
testify/mockonly for true external-system boundaries or very small/simple seams; otherwise prefer fakes or in-memory implementations - Use real containers via
testcontainers-gofor integration tests that need real Postgres, Redis, or similar infrastructure, rather than mocking the dependency - Keep tests deterministic, hermetic, and readable
Core Principles
- Behaviour over implementation - Assert externally visible outcomes
- Fast feedback - Keep most tests cheap enough to run constantly
- Deterministic - Avoid sleeps, real clocks, network access, and ambient environment dependence
- Minimal setup - Prefer small helpers over deep fixture stacks
- High signal - Each failing test should point to one clear behaviour regression
- Coverage with intent - Cover important branches, edge cases, and error paths, not just lines
Package and File Conventions
Black-Box Test Packages
Prefer:
package user_test
import (
"testing"
"github.com/stretchr/testify/require"
"example.com/project/user"
)Avoid:
package userUse same-package tests only when you intentionally need access to unexported helpers and there is no better public seam. Default to _test packages.
File Organisation
- Group tests by public API surface or behaviour
- Keep helper functions near the tests that use them unless broadly reused
- Prefer
_test.gofiles named after the subject under test, such asservice_test.go,handler_test.go,client_test.go - Avoid giant catch-all files once a package grows
Preferred Test Structure
One Top-Level Test per Behaviour, Subtests per Scenario
For a public function, prefer one top-level test and scenario subtests instead of many separate TestXxx_Yyy functions.
Use t.Run even without a table when the scenarios are clearer as explicitly written examples.
func TestCreateUser(t *testing.T) {
t.Parallel()
t.Run("success", func(t *testing.T) {
t.Parallel()
got, err := user.Create("alice@example.com")
require.NoError(t, err)
assert.Equal(t, "alice@example.com", got.Email)
})
t.Run("rejects invalid email", func(t *testing.T) {
t.Parallel()
_, err := user.Create("not-an-email")
require.Error(t, err)
require.ErrorIs(t, err, user.ErrInvalidEmail)
})
}func TestParseUserID(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
want int
wantErr string
}{
{name: "valid", input: "42", want: 42},
{name: "empty", input: "", wantErr: "empty user id"},
{name: "invalid", input: "abc", wantErr: "invalid user id"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := user.ParseUserID(tt.input)
if tt.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}Why This Pattern
- Keeps all scenarios for one behaviour together
- Makes it easy to add edge cases without duplicating setup
- Produces better output in
go test -runand CI logs - Works naturally with table-driven tests and
t.Parallel()
Parallel Test Execution
Default Rule
Call t.Parallel() unless there is a concrete reason not to.
Good candidates:
- Pure functions
- Tests using isolated in-memory state
- Tests using
t.TempDir() - Tests with per-test
httptest.Server - Table-driven subtests with isolated fixtures
Avoid or carefully isolate parallelism when tests:
- Mutate global variables, singleton state, env vars, or process working directory
- Depend on wall-clock timing or
time.Sleep - Reuse shared mocks/fakes with non-thread-safe state
- Use fixed ports, shared files, or shared databases without isolation
Parallel Subtest Safety
In Go 1.22+ modules, loop variables declared in the for statement are created per iteration, so rebinding like tt := tt is usually not needed.
Still rebind when:
- the module/package targets pre-Go-1.22 semantics
- the variable is declared outside the loop and reused
- you intentionally want compatibility with older Go module targets
For modern Go:
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// use tt safely in Go 1.22+
})
}For older module targets:
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// use tt safely
})
}Testify Usage
Prefer Testify for clearer assertions.
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)require vs assert
- Use
requirewhen the test cannot continue after failure - Use
assertwhen you want multiple related checks in one scenario
func TestUserName(t *testing.T) {
t.Parallel()
u, err := user.New("alice")
require.NoError(t, err)
assert.Equal(t, "alice", u.Name())
assert.True(t, u.IsActive())
}Prefer semantic assertions when available:
require.ErrorIsassert.Equalassert.ElementsMatchassert.Lenassert.Empty/assert.NotEmptyassert.WithinDurationassert.JSONEq
Error Testing
- Prefer asserting exported sentinel/type behaviour with
ErrorIs/ErrorAs - Avoid brittle full-string equality unless the exact message is part of the contract
- Assert both success and failure paths for public APIs
require.ErrorIs(t, err, user.ErrNotFound)Good Coverage Practices
Aim for coverage that reflects risk and behaviour, not vanity percentages.
Always Cover
- Happy path
- Input validation failures
- Boundary values
- Zero values and empty collections
- Domain-specific edge cases
- External dependency failures
- Serialization/parsing failures
- Context cancellation / timeout paths when relevant
- Idempotency or retry semantics when relevant
Coverage Heuristics
- Add a test for every bug fix before or with the fix
- Cover each exported public method/function with at least one success and one failure/edge scenario where meaningful
- Prefer focused unit tests for combinatorial logic and a smaller number of integration tests for end-to-end confidence
- If a branch matters to production behaviour, it deserves an assertion
- Remove duplicate tests that do not add behavioural signal
Coverage Commands
go test ./...
go test ./... -cover
go test ./... -coverprofile=coverage.out
go tool cover -func=coverage.out
go tool cover -html=coverage.outIntegration vs Unit Tests
Use the lightest test that proves the behaviour.
Prefer Unit Tests For
- Pure transformations and validation
- Orchestration logic with fake dependencies
- Error mapping and branching logic
Prefer Integration Tests For
- SQL queries and repository behaviour
- HTTP handlers, middleware, and routing
- Serialization boundaries
- Interactions with real adapters that are cheap and deterministic to run locally/CI
When an integration test gives stronger confidence with similar complexity, prefer it over elaborate mocking. See Integration Testing with Testcontainers-go below for container-backed patterns against real Postgres, Redis, and similar services.
Integration Testing with Testcontainers-go
Use testcontainers-go when an integration test needs a real Postgres, Redis, Kafka, NATS, or other containerised dependency instead of a mock — for verifying SQL, migrations, indexes, transactions, locking, driver behaviour, or cache integration against the real thing.
Default Stance
Unless there is a clear reason not to, prefer these defaults:
- Use real containers for integration tests, not mocks
- Prefer module-specific helpers like
postgres.Runandredis.Runover lower-level generic setup when a module exists - Prefer
testcontainers.Runover olderGenericContainerpatterns for generic services - Register cleanup immediately with
testcontainers.CleanupContainer(t, ctr)in normal tests - For expensive services like Postgres and Redis, prefer one container per package/test binary, not one container per test
- Keep tests parallel by isolating state inside the shared container per test
- Use per-test transactions, schemas, databases, logical Redis DBs, or key prefixes instead of sharing mutable state
- Let Docker assign random host ports; do not hardcode host ports in tests
- Configure wait strategies explicitly when readiness is not guaranteed by the module
- Avoid cross-test and cross-package reuse by default; keep tests hermetic and self-contained
Terminology: Containers Are Not Mocks
For Postgres and Redis integration tests, prefer real Postgres and Redis containers.
These are not mocks.
- Mocks/fakes are best for unit tests and narrow seams
- Testcontainers are best when you want confidence in the real dependency behaviour
If the test is meant to validate SQL, migrations, transactions, Redis commands, TTL behaviour, or networked dependency behaviour, a real container is usually the right tool.
Version and API Notes
github.com/testcontainers/testcontainers-gois the main library- Prefer modern
Run(...)APIs GenericContaineris the older style; prefertestcontainers.Run(...)for new generic-container examples- Module-level
RunContainer(ctx, opts...)helpers are deprecated; preferpostgres.Run(...),redis.Run(...), etc. WithReuseByName(...)is experimental; do not make it your default CI/test strategy
Lifecycle Scope Tradeoffs
Choose container lifetime deliberately:
| Scope | Startup cost | Isolation | Parallel friendliness | Recommended use |
| Per test | Highest | Strongest | Excellent | Small suites, destructive tests, tests that mutate process-wide server state |
| Per package | Moderate | Strong if you isolate test data | Excellent when state is isolated correctly | Default for Postgres, Redis, and similar services |
| Cross-package/shared reusable container | Lowest warm-start cost | Weakest | Risky | Local experimentation only; avoid as the default |
Recommended Default
For Postgres and Redis, prefer:
- one container per package/test binary
- parallel tests inside that package
- per-test state isolation inside the container
Why this is the usual sweet spot:
- Starting one Postgres/Redis container per test is often too slow
- Sharing one container per package keeps startup cost acceptable
go testruns each package in a separate process, so package-scoped fixtures already isolate one package from another- You still need to isolate state between tests inside that package
If most tests in the package need the dependency, TestMain is a good fit. If only some tests need it, a lazy package-scoped helper can be better.
Generic Container Startup
Use generic startup when there is no higher-level module or when you need custom behaviour.
package cache_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
func TestWithGenericRedis(t *testing.T) {
t.Parallel()
ctx := context.Background()
ctr, err := testcontainers.Run(ctx,
"redis:7",
testcontainers.WithExposedPorts("6379/tcp"),
testcontainers.WithWaitStrategy(
wait.ForListeningPort("6379/tcp"),
wait.ForLog("Ready to accept connections").WithStartupTimeout(30*time.Second),
),
)
testcontainers.CleanupContainer(t, ctr)
require.NoError(t, err)
endpoint, err := ctr.Endpoint(ctx, "")
require.NoError(t, err)
_ = endpoint // pass to your client under test
}Use this pattern for services without a dedicated module, or when you need full control over files, env vars, commands, or custom wait strategies.
Postgres Module Example
Prefer the Postgres module for Postgres integration tests.
package repo_test
import (
"context"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/postgres"
)
func TestRepositoryWithPostgres(t *testing.T) {
t.Parallel()
ctx := context.Background()
ctr, err := postgres.Run(ctx,
"postgres:16-alpine",
postgres.WithDatabase("app_test"),
postgres.WithUsername("postgres"),
postgres.WithPassword("postgres"),
postgres.WithInitScripts(filepath.Join("testdata", "init.sql")),
postgres.BasicWaitStrategies(),
)
testcontainers.CleanupContainer(t, ctr)
require.NoError(t, err)
dsn, err := ctr.ConnectionString(ctx, "sslmode=disable")
require.NoError(t, err)
_ = dsn // open your DB client here
}Notes:
postgres.BasicWaitStrategies()is the common default for Postgres readinessWithInitScripts(...)is useful for schema setup or seed dataConnectionString(...)is usually the easiest way to build a DB client
Redis Module Example
Prefer the Redis module for Redis integration tests.
package cache_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
tcredis "github.com/testcontainers/testcontainers-go/modules/redis"
)
func TestCacheWithRedis(t *testing.T) {
t.Parallel()
ctx := context.Background()
ctr, err := tcredis.Run(ctx,
"redis:7",
)
testcontainers.CleanupContainer(t, ctr)
require.NoError(t, err)
uri, err := ctr.ConnectionString(ctx)
require.NoError(t, err)
_ = uri // pass to your redis client under test
}Notes:
- Use module options like
WithConfigFile(...),WithTLS(), orWithLogLevel(...)when relevant ConnectionString(...)returns a ready-to-use Redis URIWithSnapshotting(...)configures Redis persistence behaviour; it is not a per-test isolation/reset mechanism
Package-Scoped Container Pattern
When most tests in a package need the same dependency, prefer a package-scoped container.
package repo_test
import (
"context"
"log"
"os"
"testing"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/postgres"
)
var (
postgresDSN string
postgresCtr *postgres.PostgresContainer
)
func TestMain(m *testing.M) {
ctx := context.Background()
var err error
postgresCtr, err = postgres.Run(ctx,
"postgres:16-alpine",
postgres.WithDatabase("app_test"),
postgres.WithUsername("postgres"),
postgres.WithPassword("postgres"),
postgres.BasicWaitStrategies(),
)
if err != nil {
log.Fatal(err)
}
postgresDSN, err = postgresCtr.ConnectionString(ctx, "sslmode=disable")
if err != nil {
log.Fatal(err)
}
code := m.Run()
if err := testcontainers.TerminateContainer(postgresCtr); err != nil {
log.Printf("terminate postgres container: %v", err)
}
os.Exit(code)
}This is a good default when:
- the container startup cost is noticeable
- most tests in the package need the dependency
- you have a clear per-test isolation strategy for data
Parallel Tests Need State Isolation, Not Just Container Isolation
Sharing one container does not make parallel tests safe by itself.
If tests run with t.Parallel(), they must not step on the same data.
Prefer one of these per-test isolation patterns.
Postgres Isolation Patterns
Preferred order:
- Per-test transaction + rollback when your code can run inside an injected transaction
- Per-test schema when your app can point each test at its own schema/search path
- Per-test database inside the same Postgres server when schema isolation is not enough
- Per-test container only when the test truly needs process-level or cluster-level isolation
Transaction pattern
Best when the application code can accept a *sql.Tx, pgx.Tx, or a narrow query interface.
- Fastest reset strategy
- Excellent for parallel tests
- Minimal container churn
Per-schema pattern
Good when each test can use a unique schema name.
- Works well with one package-level Postgres container
- Lets parallel tests run without dropping each other's tables
- Usually faster than creating a fresh container per test
Snapshot/Restore pattern
The Postgres module supports Snapshot(...) and Restore(...).
Use it when:
- migrations are expensive
- you want a quick reset to a known base state
- tests are mostly serial, or you can guarantee only one restore operation at a time
Be careful:
Restore(...)resets shared database state- do not call it concurrently from parallel tests that share the same Postgres container/database
- for truly parallel tests, prefer per-test transactions, schemas, or databases instead
Redis Isolation Patterns
Preferred order:
- Per-test logical DB when using standalone Redis and your client can select DB numbers
- Per-test key prefix/namespace when logical DB separation is not practical
- Per-test container when you need hard isolation or destructive global operations
Per-test logical DB
Good when using a standard standalone Redis image.
client := redis.NewClient(&redis.Options{
Addr: redisAddr,
DB: testDBNumber,
})Guidance:
- Give each parallel test a distinct DB number
- Clean up with
FLUSHDBfor that logical DB only - Avoid
FLUSHALLin tests
Per-test key prefix
Good when DB-number isolation is unavailable or inconvenient.
- Prefix keys with a test-unique namespace such as
t_<id>: - Delete only that namespace in cleanup
- Safer for parallel tests than sharing raw keys
Be careful with global Redis operations:
FLUSHALLwill destroy every test's stateFLUSHDBis also unsafe if multiple parallel tests share the same logical DB
Example: Package-Level Postgres + Parallel Tests
This is usually the best tradeoff for repository/service integration tests.
- Start one Postgres container in
TestMain - Run migrations once
- Let each parallel test use its own transaction, schema, or database
Prefer this over one-container-per-test when:
- container startup dominates runtime
- the main thing you need is data isolation, not full server-process isolation
Example: Package-Level Redis + Parallel Tests
This is usually the best tradeoff for cache integration tests.
- Start one Redis container for the package
- Give each test its own logical DB or unique key prefix
- Never use global destructive cleanup across all tests
Wait Strategies and Readiness
Do not assume container start means service readiness.
Prefer:
- module-provided readiness helpers when available
wait.ForListeningPort(...)for services that only need the socket upwait.ForLog(...)when service logs are the most reliable readiness signal- combined strategies for flaky services or non-Linux host setups
Postgres specifically benefits from explicit readiness checks like postgres.BasicWaitStrategies().
Ports, Addresses, and Connection Strings
Prefer runtime discovery over fixed ports:
ctr.ConnectionString(ctx, ...)for Postgresctr.ConnectionString(ctx)for Redisctr.Endpoint(ctx, "")orctr.MappedPort(ctx, ...)for generic containers
Do not hardcode localhost:5432 or localhost:6379.
Parallel tests depend on Docker assigning distinct mapped host ports.
Cleanup Guidance
In normal tests:
ctr, err := postgres.Run(ctx, "postgres:16-alpine", postgres.BasicWaitStrategies())
testcontainers.CleanupContainer(t, ctr)
require.NoError(t, err)In TestMain:
- use
testcontainers.TerminateContainer(...)explicitly afterm.Run()
Register cleanup immediately after startup.
Reuse Guidance
WithReuseByName(...) exists, but it is experimental.
Avoid making reuse your default because it:
- weakens test hermeticity
- risks hidden state between runs
- can behave differently in local development vs CI
Prefer clean startup/teardown unless you have a deliberate local-only optimisation strategy.
Quick Templates
Postgres package template
var postgresDSN string
func TestMain(m *testing.M) {
ctx := context.Background()
ctr, err := postgres.Run(ctx,
"postgres:16-alpine",
postgres.WithDatabase("app_test"),
postgres.WithUsername("postgres"),
postgres.WithPassword("postgres"),
postgres.BasicWaitStrategies(),
)
if err != nil {
log.Fatal(err)
}
postgresDSN, err = ctr.ConnectionString(ctx, "sslmode=disable")
if err != nil {
log.Fatal(err)
}
code := m.Run()
_ = testcontainers.TerminateContainer(ctr)
os.Exit(code)
}Then make each parallel test isolate its own transaction, schema, or database.
Redis package template
var redisURI string
func TestMain(m *testing.M) {
ctx := context.Background()
ctr, err := tcredis.Run(ctx, "redis:7")
if err != nil {
log.Fatal(err)
}
redisURI, err = ctr.ConnectionString(ctx)
if err != nil {
log.Fatal(err)
}
code := m.Run()
_ = testcontainers.TerminateContainer(ctr)
os.Exit(code)
}Then give each parallel test its own logical DB or key prefix.
Mocking Guidance
Default Rule
Mock only external systems or very thin boundaries. Prefer fakes and in-memory implementations for richer domain behaviour.
Prefer:
- In-memory repositories
httptest.Server- Temporary directories
fstest.MapFS- Fixed clocks / injectable time sources
Reach for testify/mock when:
- The dependency is an external system boundary
- A fake would be more complex than the behaviour being tested
- You need precise call assertions on a thin collaborator
Avoid mocks for:
- Your own core domain models
- Simple data containers
- Behaviour that is easier to express with an in-memory fake
- Deep implementation-driven interaction testing
Testify Mock Pattern
type MockMailer struct {
mock.Mock
}
func (m *MockMailer) Send(ctx context.Context, msg mail.Message) error {
args := m.Called(ctx, msg)
return args.Error(0)
}
func TestService_SendWelcomeEmail(t *testing.T) {
t.Parallel()
mailer := new(MockMailer)
mailer.
On("Send", mock.Anything, mail.Message{To: "alice@example.com"}).
Return(nil).
Once()
svc := user.NewService(mailer)
err := svc.SendWelcomeEmail(context.Background(), "alice@example.com")
require.NoError(t, err)
mailer.AssertExpectations(t)
}Rules:
- One mock per external dependency seam, not everywhere
- Keep expectations minimal and behaviour-focused
- Avoid over-asserting call order unless it is part of the contract
- Do not share mock instances across parallel tests
Filesystem Testing
Prefer standard-library-friendly seams and hermetic storage.
Recommended Patterns
- Inject an
fs.FSfor read-only filesystem behaviour - Use
fstest.MapFSfor small read scenarios - Use
t.TempDir()for realistic file creation/update flows - Keep filesystem access behind a small adapter if the production code is otherwise hard to isolate
fstest.MapFS Example
func TestLoadConfig(t *testing.T) {
t.Parallel()
files := fstest.MapFS{
"config.json": {Data: []byte(`{"env":"test"}`)},
}
cfg, err := config.Load(files, "config.json")
require.NoError(t, err)
assert.Equal(t, "test", cfg.Env)
}t.TempDir() Example
func TestWriteReport(t *testing.T) {
t.Parallel()
dir := t.TempDir()
path := filepath.Join(dir, "report.txt")
err := report.Write(path, "hello")
require.NoError(t, err)
data, err := os.ReadFile(path)
require.NoError(t, err)
assert.Equal(t, "hello", string(data))
}Avoid mocking os calls directly when a temp dir or fs.FS seam would be simpler.
HTTP Client Testing
Prefer real HTTP semantics without real network dependencies.
Recommended Patterns
- Use
httptest.Serverto test client behaviour against realistic responses - Inject
*http.Clientinto your code - For very small seams, a custom
http.RoundTripperfake can be enough - Mock higher-level gateways only when HTTP itself is not the thing under test
httptest.Server Example
func TestClient_GetUser(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodGet, r.Method)
assert.Equal(t, "/users/42", r.URL.Path)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":42,"name":"alice"}`))
}))
t.Cleanup(srv.Close)
client := api.NewClient(srv.URL, srv.Client())
got, err := client.GetUser(context.Background(), 42)
require.NoError(t, err)
assert.Equal(t, "alice", got.Name)
}Custom Transport Fake
For narrow client logic, prefer a tiny fake over a heavyweight mock:
type roundTripFunc func(*http.Request) (*http.Response, error)
func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
return f(r)
}Use this when you only need to simulate one request/response pair and httptest.Server would be unnecessary ceremony.
Time Testing
Do not depend on real time in tests.
Recommended Patterns
- Inject time via a function like
now func() time.Time - Or define a tiny clock interface at the boundary that needs it
- Use fixed timestamps in tests
- Avoid
time.Sleepas a synchronisation mechanism - Prefer contexts, channels, and explicit signals over waiting for elapsed time
Function Injection Example
type Service struct {
now func() time.Time
}
func NewService() *Service {
return &Service{now: time.Now}
}func TestTokenExpired(t *testing.T) {
t.Parallel()
fixed := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
svc := token.NewService(func() time.Time { return fixed })
assert.True(t, svc.IsExpired(fixed.Add(-time.Second)))
assert.False(t, svc.IsExpired(fixed.Add(time.Second)))
}If production code currently calls time.Now() directly in many places, first introduce a small seam at the package boundary rather than mocking time per call site.
Table-Driven Testing Guidance
- Prefer plain
t.Runsubtests first without a table when scenarios are clearer as explicitly written examples - Use tables when the setup is mostly the same and only inputs/expectations vary
- Name cases for behaviour, not raw input values
- Keep test case structs focused; avoid giant anonymous structs with dozens of fields
- Split unrelated behaviours into separate tests instead of massive tables
Good fit for table-driven tests:
- Parsing and validation matrices
- Boundary-value checks on the same function
- Multiple related input/output combinations for deterministic logic
Poor fit for table-driven tests:
- Scenarios with materially different setup or assertions
- Behaviour tests that read more clearly as individually named
t.Runblocks - Large tables that hide intent behind many mostly-empty fields
Test Helpers
- Prefer helper functions that build valid defaults and allow explicit overrides
- Mark helpers with
t.Helper() - Return concrete values rather than hidden global state
- Keep helpers local to the package unless broad reuse justifies promotion
func newTestUser(t *testing.T, opts ...UserOption) user.User {
t.Helper()
u, err := user.New("alice", opts...)
require.NoError(t, err)
return u
}Anti-Patterns
Avoid these unless there is a strong reason:
General
- Same-package white-box tests by default
- Multiple top-level tests for each small scenario of one function
- Reaching for table-driven tests when explicit
t.Runscenarios are clearer - No
t.Parallel()without reason - Real sleeps, real network calls, or dependence on current wall-clock time
- Assertion on internal private fields in black-box tests
- Brittle exact error string checks for wrapped/domain errors
- Massive shared fixtures that hide what the test actually needs
- Mock-heavy tests that mirror implementation rather than behaviour
- Coverage chasing without meaningful assertions
Testcontainers-go
- One heavy Postgres container per test when a package-scoped container plus data isolation would do
- Sharing one Postgres database across parallel tests with no transaction/schema/database isolation
- Sharing one Redis logical DB across parallel tests and calling
FLUSHDB - Calling
FLUSHALLin a parallel suite - Hardcoding
localhost:5432orlocalhost:6379 - Relying on container startup without readiness checks
- Treating
WithSnapshotting(...)on Redis as a test reset mechanism - Defaulting to
WithReuseByName(...)in CI - Using mocks to test SQL or Redis command behaviour that should be verified against the real service
Review Checklist
When reviewing Go tests, check for:
General
- Black-box
_testpackage usage by default t.Parallel()in top-level tests and subtests unless unsafe- Subtests for multiple scenarios of one behaviour
testify/assertandtestify/requireused appropriately- Fakes preferred over mocks when practical
testify/mocklimited to external boundaries or simple seams- No hidden global state or cross-test coupling
- Deterministic handling of filesystem, HTTP, and time
- Meaningful edge-case and error-path coverage
- Tests that read like executable specifications of public behaviour
Testcontainers-go
- Module helpers used where available (
postgres.Run,redis.Run) - Modern
Run(...)APIs instead of deprecated patterns - Cleanup registered immediately
- No fixed host ports
- Explicit readiness/wait strategy where needed
- Package-scoped lifecycle chosen deliberately
- Clear per-test isolation strategy for parallel tests
- No global destructive cleanup that breaks parallel runs
- Real Postgres/Redis containers used for integration tests instead of mocks
- Postgres snapshot/restore used only when its shared-state tradeoff is acceptable
Other versions46 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 46
- samber/cc-skills-golang★ 3.6ksame name
- sazardev/goca★ 299same name
- loulanyue/awesome-claude-notes★ 272same name
- unxed/f4★ 247same name
- Fmarzochi/EGC★ 61same name
- jmrplens/gitlab-mcp-server★ 45same name
- jmrplens/libgen-mcp★ 28same name
- castle-x/skills-x★ 18same name
- klawisha012/agentsync★ 18same name
- alexastrum/skl★ 11same name
36 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
Used alongsideProjects with it also use golang-samber-lo, golang-error-handling and 10 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
guynhsichngeodiecSkillgolang-samber-lo
Functional programming helpers for Golang using samber/lo — 500+ type-safe generic functions for slices, maps, channels, strings, math, tuples, and concurrency (Map, Filter, Reduce, GroupBy, Chunk, Flatten, Find, Uniq, etc.). Core immutable package (lo), concurrent variants (lo/parallel aka lop), in-place mutations (lo/mutable aka lom), lazy iterators (lo/it aka loi for Go 1.23+), and experimental SIMD (lo/exp/simd). Apply when using or adopting samber/lo, when the codebase imports github.com/samber/lo, or when implementing functional-style data transformations in Go. Not for streaming pipelines (→ See golang-samber-ro skill).
Also in 9 projects that use this
tquangkhai98Skillgolang-error-handling
Idiomatic Golang error handling — creation, wrapping with %w, errors.Is/As, errors.Join, custom error types, sentinel errors, panic/recover, the single handling rule, structured logging with slog, HTTP request logging middleware, and samber/oops for production errors. Built to make logs usable at scale with log aggregation 3rd-party tools. Apply when creating, wrapping, inspecting, or logging errors in Go code.
Also in 13 projects that use this
tquangkhai98Skillgolang-concurrency
Golang concurrency patterns. Use when writing or reviewing concurrent Go code involving goroutines, channels, select, locks, sync primitives, errgroup, singleflight, worker pools, or fan-out/fan-in pipelines. Also triggers when you detect goroutine leaks, race conditions, channel ownership issues, or need to choose between channels and mutexes.
Also in 13 projects that use this
alexastrumSkillgolang-swagger
Golang OpenAPI/Swagger documentation with swaggo/swag — annotation comments (@Summary, @Param, @Success, @Router, @Security), swag init code generation, framework integrations (gin, echo, fiber, chi, net/http), security definitions (Bearer/JWT, OAuth2, API key), and struct tags (swaggertype, enums, example, swaggerignore). Apply when adding or maintaining Swagger/OpenAPI docs in a Go project, or when the codebase imports github.com/swaggo/swag, github.com/swaggo/gin-swagger, github.com/swaggo/echo-swagger, github.com/swaggo/http-swagger, or github.com/swaggo/files.
Also in 7 projects that use this
guynhsichngeodiecSkillgolang-samber-mo
Monadic types for Golang using samber/mo — Option, Result, Either, Future, IO, Task, and State types for type-safe nullable values, error handling, and functional composition with pipeline sub-packages. Apply when using or adopting samber/mo, when the codebase imports `github.com/samber/mo`, or when considering functional programming patterns as a safety design for Golang.
Also in 7 projects that use this
alexastrumSkillgolang-google-wire
Compile-time dependency injection in Golang using google/wire — wire.NewSet, wire.Build, wire.Bind (interface→concrete), wire.Struct, wire.Value, wire.InterfaceValue, wire.FieldsOf, cleanup functions, //go:build wireinject injector files, and generated wire_gen.go. Apply when using or adopting google/wire, when the codebase imports `github.com/google/wire`, or when wiring an application graph at compile time via `wire.Build`. For runtime DI with reflection, see `samber/cc-skills-golang@golang-uber-dig` skill.
Also in 6 projects that use this
- +4 more from guynhsichngeodiec
- +2 more from tquangkhai98
In this repository36 other files it gives its agents.
- skills/backend-best-practices/AGENTS.mdAGENTS.md
- adr-writerSkill
- application-healthchecksSkill
- backend-best-practicesSkill
- bspec-prd-discoverySkill
- bspec-story-implementationSkill
- bspec-story-reviewSkill
- bspec-story-syncSkill
- bspec-technical-architectureSkill
- bspec-workflowSkill
- conventional-commitsSkill
- copier-scaffoldSkill
- devenvSkill
- direnvSkill
- docker-composeSkill
- dockerfileSkill
- docker-setupSkill
- docs-writerSkill
- github-actions-dockerSkill
- gitleaks-setupSkill
- golang-developmentSkill
- golang-slogSkill
- playwright-devenvSkill
- project-webappSkill
- readme-writerSkill
- release-drafter-setupSkill
- release-drafterSkill
- release-notesSkill
- renovate-pr-triageSkill
- renovate-setupSkill
- structured-loggingSkill
- summarize-articleSkill
- summarize-youtube-videoSkill
- tailwind-v4Skill
- vicinae-extensionsSkill
- zensical-setupSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

