How to Remove Pollution From Go Tests
Simplicity — the art of maximizing the amount of work not done — is essential.
How can we apply this part of the manifesto to writing tests, minimise the amount of work not done, and reduce maintenance costs?
First, we need to ask if we already have the right tools for the job. If so, we can tick the box and focus on what matters — designing tests that communicate system behaviour.
Reducing operational cost
A few decades ago, I joined a company to work as an electronics technician. When I arrived at the company HQ for my onboarding week, I saw a car park full of identical vehicles. The same brand, the same model. In the regional offices, we used the same type of equipment. Same oscilloscopes, multimeters, diagnostic kits, and IBM PCs. And later, the same Nokia 2110 phones.
Why? To reduce operational costs to an absolute minimum. One vendor means dealing with the same company, following the same procedures, and sharing the same know-how. Training new employees, regardless of the branch or country, was a smooth process. It allowed us to run the business with minimal friction. Every distraction from servicing, selling, closing deals, or making products better was a waste. It was something we wanted to minimise.
Getting stuff done
The same philosophy applies to building and selling software products. What brings in money for the business are working, top-quality applications in customers’ hands. And we create them by maximising engineering time on designing, coding and testing.
Every distraction, every novelty that introduces cognitive load, elevates the operational and maintenance burden. New syntax? New DSL? New five ways of checking if we get what we want, borrowed from other languages or frameworks? No, thank you. It increases mental pollution and, quietly, step by step, fills your brain cells with irrelevant details.
One example of such pollution is duplicating testing tools. Let’s have a look at the test below.
package id
import (
	"testing"
	"github.com/stretchr/testify/assert"
)
func TestGenerate(t *testing.T) {
	result := Generate("%s_%s_%s", "test1", "test2", "test3")
	expected := "02be9e7f-a802-35d4-9e4a-6c677259a87d"
	assert.Equal(t, expected, result)
}
The test checks if the function Generate returns the correct string value. If expected matches result, the test passes. The test couldn’t be simpler. We don’t package test data into structs, and we don’t have a slice with many input values. Just two values to compare and report the result. So, where is the catch?
Removing pollution. One step at a time.
Now it’s time to ask two questions:
- What are we testing here?
- Do we need a 3rd party library to get things done?
We already answered the first one. The test compares two string values and reports the result. That’s all. The answer to the second question is a short “No”. No, we don’t need to bring in 3rd-party dependencies into the project. Just because we can, doesn’t mean we should.
Besides adhering to the KISS principle, keeping the number of external dependencies to an absolute minimum results in a simpler, easier-to-digest codebase. It also means a lower chance of introducing security issues and less burden on security scanning, generating SBOM and other documents and reports that teams must provide to customers along with working applications.
So, our plan of action includes:
- removing dependency on the testifypackage
- changing variable names to gotandwantto keep them in line with Go conventions
- using t.Errorfmethod to report possible error
After the change, we end up with the following test.
package id
import (
	"testing"
)
func TestGenerate(t *testing.T) {
	got := Generate("%s_%s_%s", "test1", "test2", "test3")
	want := "02be9e7f-a802-35d4-9e4a-6c677259a87d"
	if want != got {
		t.Errorf("want %q, got %q", want, got)
	}
}
The test looks OK. We removed unnecessary dependencies and reduced cognitive load. But we are not done yet.
Since we checked out the code and are preparing a pull request, we can take a step further and apply the “Boys Scout Rule”.
What are we really testing here?
When we run the test, all we see is the function name.
go test -run ^TestGenerate$ github.com/nginx/agent/v3/pkg/id -v
=== RUN   TestGenerate
--- PASS: TestGenerate (0.00s)
PASS
ok  	github.com/nginx/agent/v3/pkg/id	0.178s
It’s not clear what behaviour the test validates. What does it generate and when? The name Generate itself tells us nothing about what behaviour we are testing.
What can we do about it?
Meet the gotestdox, a tool that helps you write descriptive names for tests. If you don’t have it on your local dev environment, grab and install it now.
go install github.com/bitfield/gotestdox/cmd/gotestdox@latest
Let’s run the same test, but this time with gotestdox.
$ gotestdox ./pkg/id/*.go
 ✔ Generate (0.00s)
 ...
Test passed. But what are we generating and when? We know that the test checks the Generate function. The function takes some arguments of type string and returns a string value representing a unique ID, UUID. But to know this, we need to open an editor and read the code. It’s hard to deduce the behaviour we test from the test name alone. And that’s what we need to improve.
In the next step, we will describe the behaviour and change the test name into a correct English sentence. How about something like “Generate returns a valid UUID for valid input”? Let’s try it.
func TestGenerate_ReturnsValidUUIDForValidInput(t *testing.T) {
	got := Generate("%s_%s_%s", "test1", "test2", "test3")
	want := "02be9e7f-a802-35d4-9e4a-6c677259a87d"
	if want != got {
		t.Errorf("want %q, got %q", want, got)
	}
}
How does the report look now?
 gotestdox ./pkg/id/*.go
 ...
 ✔ Generate returns valid UUID for valid input (0.00s)
Clear and easy to grasp, even when drinking another double espresso at 2 AM while fixing production issues.
What are the key mistakes to avoid?
- Bringing 3rd party packages to the project when the Go std library provides the same functionality
- Introducing functional duplication in the project and increasing maintenance costs
 
                    
                    