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.
Cannotaccess 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=1when 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.

