Jakub Jarosz

Security, Systems & Electrical Automation

Making Go Tests Nice & Tidy

2025-12-18 Go

From 50 Go Testing Mistakes

In the Simplify and Clarify Go Tests, we refactored the getSimplefsInstance function. Besides changing the name to reflect functionality better, we also changed the signature.

func getSimplefsInstance() (core.Storer, error) {
	return simplefs.Factory(core.CacheProvider{}, zap.NewNop().Sugar(), 0)
}

The new function is a test helper. It handles potential errors when calling simplefs.Factory, and returns an always-valid, ready-to-use store.

func newDefaultStore(t *testing.T) core.Storer {
	t.Helper()
	s, err := simplefs.Factory(core.CacheProvider{}, zap.NewNop().Sugar(), 0)
	if err != nil {
		t.Fatal(err)
	}
	return s
}

To recap, our main objective in the first refactoring phase was to simplify test setup and ensure we start tests with an always-valid store.

func TestSetAndRetrieveValueFromStore(t *testing.T) {
	store := newDefaultStore(t)
	...
}

That’s a big win. It’s like making sure your car lights and battery are working before swapping your credit card and signing up for the annual car inspection.

But there is still room for improvement. What else can we change in the cache storages module used by the Souin HTTP cache?

It all starts with describing behaviour

Now it’s time to look at test names. Do they describe tested behaviour in the simplest possible way?

func TestIShouldBeAbleToReadAndWriteDataInSimplefs(t *testing.T) {
	...
}

First, we know that we operate inside the simplefs package. There’s no advantage to repeating it in each test name. Next, instead of should, we will use simpler names — English sentences, to describe expected behaviour.

func TestSetAndRetrieveValueFromStore(t *testing.T) {
	...
}

How does the test report look when running the test with gotestdox?

$ gotestdox
github.com/darkweak/storages/simplefs:
 ✔ Set and retrieve value from store (1.00s)

Short and easy to grasp. We are going to change all names to keep them in the same, brief and descriptive style. And yes, we don’t need any 3rd-party heavy test libraries to write English sentences.

Handling errors in each step

After we’ve sorted out the test preconditions, it’s time to focus on the test logic. We need to create an entry in the store and then read it.

func TestSetAndRetrieveValueFromStore(t *testing.T) {
	store := newDefaultStore(t)
	
	err := store.Set("Test", []byte(baseValue), time.Duration(20)*time.Second)
	if err != nil {
		t.Fatal(err)
	}
	...
}

The crucial change compared to the original code is the introduction of error handling. We can’t confidently test the store.Get method without knowing whether the value was set in the store in the first place. What if store.Set returns an error, and the test continues? How could the result of the store.Get method be interpreted? Which behaviour is incorrect, setting the value, getting the value or maybe both?

You avoid confusion and wasted time debugging intermittent test steps by handling errors and terminating tests with the t.Fatal method.

Passing data to tested functions

How do we actually compare want and got values in the test? Let’s come back to the store.Set method. Its signature tells us that the key is of type string and the value is a slice of bytes. Interestingly, the key is a string literal Test and the value baseValue is a constant defined on the package level.

We will make reading the test easier and make the following changes:

  • use string literals
  • stop relying on constants
  • use variable names got and want to keep Go test conventions

The refactored function below is easier to read. We can go through the file line by line without jumping around to check which values we use. Besides, we use more common names for keys and values when setting or retrievedretrieving from a store.

func TestSetAndRetrieveValueFromStore(t *testing.T) {
	store := newDefaultStore(t)

	err := store.Set("key", []byte("123"), time.Duration(20)*time.Second)
	if err != nil {
		t.Fatal(err)
	}
	time.Sleep(1 * time.Second)

	got := store.Get("key") 
	...
}

We explicitly use the key string literal and the value 123 in the store.Set method. If the function fails to set the value and returns an error, we call t.Fatal and exit the test immediately. We won’t call the store.Get to retrieve the possibly incorrect value.

With these small changes, we:

  • are not relying on global values in tests anymore
  • can read and track the flow of data and action without unnecessary scrolling or jumping through the code
  • employes string literals that better reflect key-value store operation

What’s next? The next step is to verify how the test determines failure scenarios and how we can refactor this part to make it easier to understand. And once again, we will see how to employ the cmp package.