The Art of Clean Code: A Tale of Test Pollution in Go

The Art of Clean Code: A Tale of Test Pollution in Go

Anand Singh·
GoGolangTestClean Code

Ever had a test fail without touching the code?

One minute it passes. Next minute it flames out.

Well, I used to have this issue since the time I started dev and it still persists sometimes, if some basic rules are left unseen.

It’s the ghost in the test suite 👻- Test pollution.

In Golang - a language built for speed and concurrency, test pollution can quietly spread like ink in water. Let’s understand how it happens, how to catch it, and how to write tests that stay clean no matter what.

I will also cover one classic example in this article that I faced recently. So, let’s dive into it !

What is Test Pollution ?

Test pollution is when one test leaves behind something, that can be a changed variable, a file, a DB row and another test stumbles over it.

It breaks the golden rule:

Tests should be isolated, repeatable, and order-agnostic.

This may lead to - 

  • Flaky tests that fail randomly
  • Bugs that appear and disappear
  • Tests that only pass if run in the “right” order
  • Confusing errors that waste hours

Common Causes of test pollution

1. Global State

Go allows package-level variables, which means one test can silently change a value another test depends on.

var counter = 0

func TestA(t *testing.T) {
	counter++
}
func TestB(t *testing.T) {
	if counter != 0 {
		t.Fatal("Expected counter should be 0")
    }
}

2. Environment Variables

If one test sets an env var and doesn’t reset it, others will see the wrong value.

func TestEnv(t *testing.T) {
	os.Setenv("User", "test")
	// forgot to clean up/unset
}

3. Files or APIs

If tests write to the same file or call real APIs, they may interfere, overwriting data or exhausting limits. (** Two people editing the same google doc without talking to each other 😵‍💫).

func TestWriteFile(t *testing.T) {
	os.WriteFile("config.json", []byte("bad"), 0644)
}

4. Parallel Tests Sharing State

Go’s t.Parallel() runs tests at the same time. If they share memory, files, or ports, we can get race conditions.

5. Database Not Reset

If we insert data in one test and don’t clean up, the next test sees leftovers.

func TestAddUser(t *testing.T) {
	// inserts a user
}

func TestGetUser(t *testing.T) {
	// expects db to be empty
}

6. Dependent Tests (Bad Pattern)

Sometimes people write tests that rely on other tests to run first. This is risky.

func Test1(t *testing.T) {
	// registers user
}

func Test2(t *testing.T) {
	// logs in same user, assumes Test1 ran
}

In the above case, if Test 2 runs first it will cause Test2 to fail because of dependency from test1.


How to catch the test pollution ?

One simple command to run regularly as we keep writing tests -

go test -v -race -count=1 -shuffle=on ./...

The flag meanings are as below -

  • count=1: don’t cache results
  • shuffle=on: runs tests in random order
  • race: checks for race conditions

If running the above command leads to failing of tests randomly, we have got pollution 🚨. 

Best practices to keep test clean

1. Use t.Cleanup() to Restore State

Always reset what we change.

func TestEnvVar(t *testing.T) {
	original := os.Getenv("MODE")
	os.Setenv("MODE", "test")

	t.Cleanup(func() {
		os.Setenv("MODE", original)
	})
}

2. Reset Globals

First thing to think about is to avoid global state. But if we use it, reset it manually.

var config string

func setup() {
	config = "default"
}

func TestUpdate(t *testing.T) {
	setup()
	config = "custom"
}

func TestRead(t *testing.T) {
	setup()
	if config != "default" {
		t.Error("Expected default config")
	}
}

3. Use t.TempDir() for File Tests

This creates a fresh folder every time.

func TestFile(t *testing.T) {
	tmp := t.TempDir()
	os.WriteFile(filepath.Join(tmp, "out.txt"), []byte("data"), 0644)
}

4. Mock APIs and Databases (for unit tests)

Don’t hit real servers or DBs in unit tests. Use mocks, fakes, or in-memory versions.

5. Reset DB Between Tests

func resetDB(db *sql.DB) {
	db.Exec("DELETE FROM users")
}

6. Be Careful With t.Parallel()

Only use when tests are truly isolated. Lock shared resources if needed.

What About Integration Tests? (very valid question)

A general question in mind is now what to do in case of integration tests, where sometimes, tests need to follow a flow, like register → confirm → login. These integration tests can depend on order, but they need to be structured carefully.

Don'ts

Avoid doing this 

func Test1(t *testing.T) { /* setup */ }
func Test2(t *testing.T) { /* assumes Test1 ran */ }

DOs

We can do either of the below methods to solve this issue elegantly -

1. Group into one test with subtests

func TestUserFlow(t *testing.T) {
	t.Run("Register", func(t *testing.T) {
		// register user
	})
	t.Run("Confirm", func(t *testing.T) {
		// confirm email
	})
	t.Run("Login", func(t *testing.T) {
		// login
	})
}

2. Mock the dependent calls (*should be done only when mocking part is 3rd party call outside the project scope, otherwise it won't be true integration test)

Note - I implemented this approach recently as in my case the dependency was properly isolated with 3rd party calls, so I could mock them easily. (remember, in unit tests we mock internal components but in integration, we check all internal components should work together in harmony)

func TestConfirmEmail(t *testing.T) {
	mockUser := User{ID: "123", Email: "test@example.com", Confirmed: false}
	// Pretend user is already registered (can mock external API call)
	err := confirmEmail(mockUser)
	assert.NoError(t, err)
}


func TestLogin(t *testing.T) {
	mockUser := User{Email: "test@example.com", Password: "hashed", Confirmed: true}
	// Mock DB lookup returning confirmed user
	err := login(mockUser.Email, "plainPassword")
	assert.NoError(t, err)
}


Concluding thoughts

Clean tests are a way to more reliable and bug free codebase. A test suite isn’t just about coverage - it’s about trust. If tests only pass sometimes, they aren’t helping.

By keeping our tests clean, isolated, and predictable, we can build confidence in our code and our team.


So, go ahead and add some chaos to your next test run 😂

go test -v -race -count=1 -shuffle=on ./...

Golden concluding words - Let randomness show you what order never told you ! 😌