The ultimate Go testing Cheatsheet

This guide shows how to write and run tests in Go entirely from the terminal.

Examples use the Smart Trader backend monorepo we develop in my course Clean Architecture in Go: Build a Monorepo with MongoDB.

The repository is available at: github.com/leon-devs/trader-backend_monorepo

Specifically, we’ll reference the test cases available at this file: internal/adapters/mongo/strategies_dto_test.go

What is a test in Go?

A test is any function whose name starts with Test and takes a single parameter t *testing.T. Keep in mind that test files must end with *_test.go.

func TestFromStrategyCoreToDTO(t *testing.T) {
    t.Parallel() // (optional) Allow this test to run in parallel

    // Initialization

    // Execution

    // Validation
}

Package Choice: White-box vs Black-box testing

Go has a unique testing model: your tests live inside the same module and whether your test has access to internal/unexported functions depends entirely on the package name you choose at the top of your *_test.go file.

There are only two valid options:

White-box testing

“Test the internals of the package”.

When your test file uses the same package name as the production code, then your tests can:

  • Access unexported functions.
  • Access unexported types.
  • Access unexported fields.
  • Verify internal logic.
  • Call private helpers or private constructors.
  • Assert internal behavior and implementation details.

In other words, the test becomes like another .go file inside the same package, with full visibility.

Black-box testing

“Test the package as if you were an external consumer”.

When your test file uses the package name with a _test suffix:

package mongo_test

Then your tests:

  • Can import the package like a regular external user.
  • Can only access exported symbols.
  • Cannot access unexported structs, functions, methods and fields.
  • Force you to test from the public API perspective.
  • Prevent tight coupling to implementation details.
  • Help you refactor internals without breaking tests.

This is ideal when you want to test a package as a library, validate only the behavior but not the implementation, the public API must remain stable or when you want tests that won’t break during refactors of internal code.

Remember: Black-box testing improves maintainability!

Table-driven tests & subtests

From internal/adapters/mongo/strategies_dto_test.go:

func TestFromStrategyCoreToDTO(t *testing.T) {
    t.Parallel()

    mockedMongoID := "68e6bc21731dcf55202ad7fc"
    mockedObjectID, err := bson.ObjectIDFromHex(mockedMongoID)
    assert.Nil(t, err)

    testCases := []struct {
        Name   string
        Input  *domain.Strategy
        Output *StrategyDTO
        Error  error
    }{
        {
            Name:   "nil input returns error",
            Input:  nil,
            Output: nil,
            Error:  errors.New("invalid input strategy"),
        },
        {
            Name: "empty mongo id should not populate id",
            Input: &domain.Strategy{ ID: "", Name: "name", Description: "description" },
            Output: &StrategyDTO{ ID: bson.NilObjectID, Name: "name", Description: "description" },
            Error: nil,
        },
        {
            Name: "invalid mongo id return error",
            Input: &domain.Strategy{ ID: "invalid mongo id", Name: "name", Description: "description" },
            Output: nil,
            Error:  errors.New("invalid strategy id: 'invalid mongo id'"),
        },
        {
            Name: "strategy successfully processed",
            Input: &domain.Strategy{ ID: mockedMongoID, Name: "name", Description: "description" },
            Output: &StrategyDTO{ ID: mockedObjectID, Name: "name", Description: "description" },
            Error: nil,
        },
    }

    for _, tc := range testCases {
        t.Run(tc.Name, func(tt *testing.T) {
            tt.Parallel()

            output, err := fromStrategyCoreToDTO(tc.Input)

            if tc.Error != nil {
                assert.Nil(tt, output)
                assert.NotNil(tt, err)
                assert.EqualValues(tt, tc.Error.Error(), err.Error())
                return
            }

            assert.Nil(tt, err)
            assert.NotNil(tt, output)
            assert.EqualValues(tt, tc.Output.ID, output.ID)
            assert.EqualValues(tt, tc.Output.Name, output.Name)
            assert.EqualValues(tt, tc.Output.Description, output.Description)
        })
    }
}

Why this is idiomatic

  • A single test drives multiple scenarios via a table.
  • Each row becomes a subtest ( t.Run(tc.Name, …) ) with its own name (great for filtering).
  • t.Parallel() on the parent and inner tests speeds up the suite (combine with -race).

Configuring Long and Short tests

When you have tests cases that might be slow to execute (like integration tests going to an actual DB), you can rely on the -short flag to only execute quick tests.

func TestMaybeSlow(t *testing.T) {
    if testing.Short() {
        t.Skip("Skipping slow test in -short mode")
    }
    
    t.Cleanup(func() {
        // teardown here
    })
}

Running tests from the terminal

Run these commands from your module root (repo root). You can adjust paths as needed in your projects.

  • Run all tests in the module
go test ./...
  • Verbose output (print each test & subtest name)
go test -v ./…
  • Disable cache for flaky cases
go test -v -count=1 ./...
  • Fail fast on first failure
go test -failfast ./...
  • List available tests (by name) in a given package
go test -list . ./internal/adapters/mongo
  • Run all tests for a single package
go test -v ./internal/adapters/mongo
  • With race detector (highly recommended in parallel tests)
go test -race -v ./internal/adapters/mongo
  • Run one specific test function (by name / regex)
go test -v ./internal/adapters/mongo -run '^TestFromStrategyCoreToDTO$'
  • Run any test starting with TestFromStrategy
go test -v ./internal/adapters/mongo -run '^TestFromStrategy'
  • Run a specific subtest (table row)

Your subtests are named, e.g. “strategy successfully processed“.

Option A (partial match to avoid escaping spaces):

go test -v ./internal/adapters/mongo \
  -run '^TestFromStrategyCoreToDTO$/.*successfully processed'

Option B (exact match with spaces escaped; verbose regex):

go test -v ./internal/adapters/mongo \
  -run '^TestFromStrategyCoreToDTO$/^strategy successfully processed$'

Tip: keep subtest names unique and grep-friendly.

  • Run tests in a single file
go test -v ./internal/adapters/mongo -run '^TestFromStrategyCoreToDTO$'
  • Run only short tests (skip long/slow ones)

Mark long tests with:

if testing.Short() { t.Skip("skipping slow test in -short mode") }

Then run only short tests:

go test -short ./...

You can also combine short tests with other flags:

go test -short -race -v ./internal/adapters/mongo
  • Get coverage report
go test -cover ./...
  • Coverage profile + per-function breakdown
go test -coverprofile=coverage.out ./internal/adapters/mongo

go tool cover -func=coverage.out

You’ll see lines like:

ok  	github.com/leon-devs/trader-backend_monorepo/internal/adapters/mongo	0.647s	coverage: 24.3% of statements
  • HTML coverage report (visual)

Using a generated coverprofile file, open it as HTML running:

go tool cover -html=coverage.out -o coverage.html

And then just open coverage.html in your web browser.

  • Coverage across multiple packages in a monorepo

When tests in one package exercise code in other packages, use the -coverpkg flag:

go test -coverprofile=coverage.out \
  -coverpkg=./internal/... \
  ./internal/adapters/mongo

go tool cover -func=coverage.out

go tool cover -html=coverage.out -o coverage.html

This is very useful in monorepos so coverage properly attributes lines in ./internal/… even if you’re running tests only from ./internal/adapters/mongo.

Useful flags you’ll actually use daily

  • -v : Verbose mode (print tests/subtests).
  • -run : Run only tests/subtests that match.
  • -count=1 : Disable test result cache.
  • -race : Detect data races (slower but worth it)
  • -failfast : Stop at first failure.
  • -cover, -coverprofile=FILE : Obtain coverage reports.
  • -coverpkg=PATTERN : Include multiple packages in coverage.
  • -short : Activate short-mode so tests can skip heavy paths.

Combine them freely, as you need!

go test -race -v -cover -count=1 ./internal/adapters/mongo

Best practices (fast & reliable suites)

  • Keep subtest names descriptive and grep-friendly (you’ll filter with -run).
  • Use t.Parallel() when safe (no shared mutable state without sync).
  • Run -race regularly to catch data races early.
  • Stick to one assertion style (stdlib vs testify/assert) across the repo.
  • Use -count=1 when debugging, then remove it to let caching speed things up.
  • Mark slow paths behind testing.Short() so CI/local runs can choose.

See you on the next one!

Fede.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top