Your Go backend works locally. Then you deploy to production and something breaks — maybe a payment endpoint or a commission calculation function. The problem isn't the code you wrote, it's what you didn't verify before. We at Meteora Web see it every day: Italian SMEs treat tests as a cost, not an investment. But a bug in production costs much more than an hour of testing. In this guide, we'll show you how to use testing in Go — unit tests, benchmarks and table-driven tests — to write code that handles traffic and tight margins.
Why are unit tests in Go different from other languages?
Go was born for production. The testing toolchain is built into the language: no external frameworks, no endless configuration. The go test command does everything. But the real difference is this: tests in Go are normal code. No magic, no implicit reflection. You write a function, call it, verify the result. This means tests are readable even by those who didn't write them — and in a company that produces, readability is a form of cheap maintenance.
The most powerful pattern in Go is the table-driven test: define a table of input and expected output cases, then a single loop runs them all. It sounds simple, but it changes everything. One test per case, one maintenance point.
package main
import "testing"
func TestCalculateCommission(t *testing.T) {
tests := []struct {
name string
amount float64
expected float64
}{
{"zero amount", 0, 0},
{"small amount", 100, 2.5},
{"medium amount", 1000, 25},
{"large amount", 10000, 250},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := CalculateCommission(tt.amount)
if result != tt.expected {
t.Errorf("expected %.2f, got %.2f", tt.expected, result)
}
})
}
}This pattern lets you add a new case in one line, without duplicating code. And when a bug comes from production, the first step is write the test that reproduces it — then fix it. That bug won't come back.
Sponsored Protocol
How to structure unit tests in a real Go project
The convention is simple: a main_test.go file next to the file it tests. But in real projects with multiple packages, the structure follows the domain logic. We use a practical rule: one test for every public function, and targeted tests for private functions containing business logic. Private functions that only call other functions don't need their own tests — the public function's test covers them.
A common mistake we see in projects that come to us: tests that depend on the environment. If your test calls a database or an external API, it's not a unit test — it's an integration test. Unit tests must be fast, deterministic and isolated. If you need a database, use an interface and a mock.
type Store interface {
GetUser(id int) (User, error)
}
type MockStore struct {
user User
err error
}
func (m MockStore) GetUser(id int) (User, error) {
return m.user, m.err
}With an interface and a mock, your test verifies your code's logic, not the behavior of an external service. And if the external service changes, your test doesn't break — your code keeps working.
Sponsored Protocol
How to write benchmarks that really measure performance?
A benchmark in Go isn't an opinion — it's a measurement. The benchmarking framework is built-in and gives you precise numbers: time per operation, allocations, bytes allocated. But the right question isn't "how fast is it?", it's "how fast compared to what?" and "what does it cost in resources?".
In our work, benchmarks serve concrete decisions: an endpoint that responds in 200ms is fine, but what if traffic doubles? And if your service handles commissions for an e-commerce, a slow algorithm means waiting users — and waiting users are users who abandon their cart.
func BenchmarkCalculateCommission(b *testing.B) {
for i := 0; i < b.N; i++ {
CalculateCommission(1000)
}
}The b.N loop is managed automatically: the framework runs the function enough times to get a reliable measurement. Then run go test -bench=. and read the results. But beware: benchmarks must run in a stable environment. If your laptop is under load, the numbers aren't reliable. We always run them in CI, on a dedicated machine, with production-like load.
How to read benchmark results and make decisions
The output of go test -bench=. shows time per operation and allocations. Allocations are often the real bottleneck: every allocation is a pause for the garbage collector. If a function called millions of times allocates memory unnecessarily, the cost is huge. The benchmark tells you exactly where to intervene.
Sponsored Protocol
BenchmarkCalculateCommission-8 100000000 12.5 ns/op 0 B/op 0 allocs/opIf you see 0 allocs/op, you're good. If you see allocations, use go test -bench=. with -benchmem to see details, then optimize the function. But remember: optimizing before measuring is a losing bet. Measure first, then decide if it's worth it.
What common mistakes to avoid in Go tests?
The first mistake is not writing tests because "the code works". The second is writing tests that don't test anything — tests that always pass, regardless of the code. This happens when the test verifies the implementation, not the behavior. A test that checks internal details of a function breaks on every refactoring, even when the behavior is correct.
The third mistake is over-testing: tests covering every line of code, including impossible cases. 100% coverage is an expensive myth. We aim to cover critical business logic — calculations, decisions, flows — not trivial functions. A test for a function that returns a constant is wasted time.
The fourth mistake is ignoring tests in CI. A test that isn't run on every push is a test that doesn't exist. Configure your pipeline to run go test ./... and go vet ./... on every commit. If a test fails, the build stops. It seems rigid, but it's the only way to guarantee that code in production is the tested code.
Sponsored Protocol
How to integrate Go tests into your team's workflow?
Tests aren't a separate task: they're part of the development work. In our flow, every feature starts with defining test cases — before writing the code. This is test-driven development applied pragmatically: write the test that describes the expected behavior, then write the code that passes it. The result is simpler code, because you only write what's needed to pass the test.
For teams working on branches, the rule is: no merge without green tests. If you use Gitflow or trunk-based development, as we explained in our guide on branching strategies, tests are the quality gate. A branch with failing tests doesn't enter main. This protects production and reduces conflicts — because broken code never reaches the merge.
And when a test fails in CI, it's not a tester's problem: it's the team's problem. You solve it together, immediately, not later. In our work with SMEs, we see this discipline pays off: fewer bugs in production, less time fighting fires, more time building.
How to use code coverage without becoming its slave?
Coverage is a useful metric, but it's not the goal. go test -cover tells you what percentage of lines were executed by tests. But high coverage doesn't mean good tests — it only means you executed a lot of code. We use coverage to find dead zones: functions never tested, branches never taken. Then we decide if they're worth testing.
The practical rule: business logic must have high coverage, the rest can be lower. A commission calculation, input validation, routing decision — these must be 100% covered. A function that logs a message? No need to test it. Time spent testing trivial code is time not spent testing what generates revenue.
Sponsored Protocol
To measure coverage in CI, use go test -coverprofile=coverage.out and then go tool cover -func=coverage.out to see per-function details. If a critical function has low coverage, it's a red flag — not a number to ignore.
What to do now
Tests aren't a cost: they're an investment that pays every time you avoid a bug in production. Here are the immediate actions:
- Write a table-driven test for the most critical function in your backend — the one handling payments or calculations. Add at least 5 cases, including edge cases.
- Run
go test -coveron your project and identify functions with zero coverage. Pick one and write the missing tests. - Configure CI to run
go test ./...andgo vet ./...on every push. If you don't have CI, start with a simple local script. - Measure a benchmark on your most-called function. If it has allocations, optimize it — and verify with the benchmark that the change worked.
- Read the official documentation on testing in Go and on the testing package for deeper details.
And if you want to see how all this fits into a complete backend project, check out our guide on Go for backend — there you'll find the full picture, from concurrency to REST APIs. We at Meteora Web build backends that handle traffic and margins — and tests are the first step.