Skip to main content

Testing in Go: Naming Conventions

·10 mins

Programming languages and tools often have conventions. These conventions help make our work more straightforward. Just like all tools out there, Go also has some conventions when it comes to testing. Some are defined in the language itself (official), while others are coined by community members (unofficial).

In this article we will look at the naming conventions for files, functions and variables separately.

Two people opening a disproportionately big box

File and package naming conventions #

Go’s testing package comes with an expectation that any test file must have a _test.go suffix. For example, if we would have a file called person.go its test file must be named person_test.go. This is due to the package building process, where Go knows to ignore these files when building the package due to their naming. Simply, it ignores test files as they are not needed for the program to run.

Additionally, Go ships with a command line tool called go test. This tool automates testing the packages named by the import paths. It recompiles each package along with any files with names that match the file pattern *_test.go. This means that go test recognizes these files as special and compiles them as a separate package, and then links and runs them with the main test binary.

When it comes to packages, Go by default expects that all test files are part of the same package that they test. For example, if person.go defines a person package, the respective person_test.go should also be part of the person package. This also means that both, the person.go and person_test.go files should be placed in the same directory - we let Go worry what files should be loaded depending on what go command we run.

Looking at Golang’s source code oddly I found some disregard for these rules. For example, the tests for the fmt package in the standard library, belong to a fmt_test package, instead of the fmt package.

At first, my observation was that this is wrong and for some reason it is not fixed yet. After a more thorough research it was obvious that this is an intentional approach and not a mistake. As explained in this Stack Overflow answer, the best way to look at this is to differentiate the two approaches as “black box” and “white box” testing.

The black box approach, where the test and the production code are in separate packages, allows us to test only the exported identifiers of a package. This means that our test package will not have access to any of the internal functions, variables or constants that are in the production code.

The white box approach, where the test and the production code are in the same package, allows us to test both the non-exported and expored identifiers of the package. This is the preferrable approach when writing unit tests that require access to non-exported variables, functions, and methods.

I personally find the white box approach preferrable, because this is the default behaviour of the tooling that ships with the language. We as users of said tooling should employ good judgement and conventions to write code that is testable and avoid touching non-exported identifiers in the tests. In other words, we should adhere to the defaults, unless we have a really good reason not to.

In any case, if you would like to learn how to idiomatic Go and how to organise your packages properly, the source code of the language is the best place to learn from.

Person thinking about shapes

Function naming conventions #

While the file naming convention is enforced by the language and its toolkit, test function naming conventions are loosely enforced by Go, but are community driven.

In Go, each test file is composed of one or many test functions. Each test function has the following signature structure:

func TestXxx(*testing.T)

What’s important to notice is that Xxx does not start with a lowercase letter. The function name serves to identify the test routine. A simple test function looks like this (stolen from here):

func TestAbs(t *testing.T) {
    got := Abs(-1)
    if got != 1 {
        t.Errorf("Abs(-1) = %d; want 1", got)
    }
}

And that’s all that’s enforced by Go and it’s toolkit. Still, there are a few common ways to name your test functions. For example, we have a simple type Person with age attribute. It receives a function older which checks what if one Person is older than another Person, by comparing their age attributes.

package main

type Person struct {
	age  int64
}

func (p *Person) older(other *Person) bool {
	return p.age > other.age
}

We would write a test function for older, looking like this:

package main

import (
	"fmt"
	"testing"
)

func TestOlder(t *testing.T) {
	p1 := &Person{21}
	p2 := &Person{22}

	if !p1.older(p2) {
		t.Fatalf(fmt.Sprintf("Expected %d > %d", p1.age, p2.age))
	}
}

Here we name our test function TestOlder, which clearly states the function under test - older. This is in line with what the testing package expects - a PascalCased function name, starting with Test. What comes after is up to us.

In our small example, calling the test function TestOlder is the most common approach that you will see in the wild. But, what if we want to test the same function (older) in more test functions? Do we use TestOlder1, TestOlder2, etc. as test function names? Or is there a better way?

For such scenarios, I have found a few approaches in the wild:

The Golang source code itself has a naming convention. If we zero in on an example, like the test where the strings.Compare function is tested, we can see the convention in action:

  1. The base test function follows the format of Test + the name of the function under test. For example TestCompare, which tests the Compare function.
  2. More specific tests, for example a test that compares two idential strings, is called TestCompareIndentialStrings. Tests that are more specific express that in the name, using the PascalCase naming scheme.

We can see the same pattern in other files, for example in flag_test.go, where the functionality of the flags package is tested. Notable examples there are the TestUserDefined and TestUserDefinedForCommandLine test functions:

func TestUserDefined(t *testing.T) {
	// Snipped...
}

func TestUserDefinedForCommandLine(t *testing.T) {
	// Snipped...
}

Moving on to another popular Golang project, consul by HashiCorp, we can see a different test functions naming convention. If we look at the api_test.go file, where the API client is tested, we can see that the project uses a naming convention where:

  1. The base test function follows the format of Test + the name of the package where the function is placed, with the function name appended after an underscore (_). For example, the API.Debug.Heap() function is tested in the TestAPI_DebugHeap test function. Similarly, the API.SetupTLSConfig is tested in TestAPI_SetupTLSConfig test function.
  2. There are functions like API.Agent.Services() that require more specific tests. That’s why, for example there are TestAPI_AgentServices and TestAPI_AgentServicesWithFilter, where in the latter there is more specific functionality being tested.
func TestAPI_AgentServices(t *testing.T) {
	// Snipped...
}

func TestAPI_AgentServicesWithFilter(t *testing.T) {
	// Snipped...
}

These are just a few examples of test function naming conventions, so expect to find some others in the ecosystem. There are various conventions when it comes to naming testing functions, but all of them have to follow the basic format of TestXxx that the testing package enforces.

Variable naming conventions #

While there is strict enforcement of the file name convention, and a loose enforcement of test function naming, things are very relaxed when it comes to variables naming. Basically, Golang does not enforce any conventions on the naming of the variables that we can use in our tests via the tooling.

This in theory means that everyone can come up with their own variable names. But, what does that mean in practice? What do popular open source projects do when it comes to naming variables?

Before we dive in any open source projects, we have to go back to the basics. I am not sure if this is well known (enough), as I have found it a bit too burried in the Github wiki, but Go has a nice “Go Code Review Comments” page where variable names are discussed.

While the section is short, it says a lot about how we should be naming our variables:

Variable names in Go should be short rather than long. This is especially true for local variables with limited scope. Prefer c to lineCount. Prefer i to sliceIndex.

This part is self-explanatory. Go errs on the side of short variable names. In my personal opinion this does not make sense in a time of very powerful text editors that autocomplete our code. I prefer to be lazy and read what each variable means than figuring out what c, t or p mean. Still, if you believe in consistency, we should all follow the same guidelines when writing Go.

Futher, it says:

The basic rule: the further from its declaration that a name is used, the more descriptive the name must be.

This is something I personally like as a rule – the cognitive weight should be small when regaining context of what a variable or concept means. In such cases, configuration (or conf) can do wonders compared to c.

Lastly, it states:

Common variables such as loop indices and readers can be a single letter (i, r). More unusual things and global variables need more descriptive names.

The i, j & k variable names for indices are very commonly used, especially in C-inspired languages, so if you are a little bit experienced (or been exposed) to them this will be expected.

But, are we bikeshedding here? Why are we discussing variable names, does it matter that much?

Well, Go tests are just code. Being code, we should expect that all tests follow these guidelines just like all other code does. Also, we should write our tests using these guidelines so our tests feel familiar to others that will work with them. There shouldn’t be a major change of code style when switching between business logic and tests.

Now, let’s go back to popular open source projects writen in Go:

Looking at Terraform, another popular HashiCorp project, one thing that is ubiquitous about its test suite is that the project has both, the actual/expected and the got/want naming convention when it comes to test failures.

Basically, the test case has an expected value, which is what we expect the function under test to return. The actual value is what the function under test returned. What is convenient about following that naming convention is that expected and actual clearly state that those are the values that will have to be compared with each other, which will drive the decision if the test will pass or not:

// From: https://github.com/hashicorp/terraform/blob/250527d923f07130c36e65f9bb43b58fcbfe66cf/httpclient/useragent_test.go#L41-L43
if c.expected != actual {
  t.Fatalf("Expected User-Agent '%s' does not match '%s'", c.expected, actual)
}

Another way to achieve the same is the got/want naming, where the comparison looks like:

// From: https://github.com/hashicorp/terraform/blob/250527d923f07130c36e65f9bb43b58fcbfe66cf/backend/unparsed_value_test.go#L34-L45
if got, want := diags[3].Description().Summary, undeclPlural; got != want {
  t.Errorf("wrong summary for diagnostic 3\ngot:  %s\nwant: %s", got, want)
}

The idea behind the naming is the same, but the goal is to fail with a practical message to whoever’s debugging your code in the future.

From here on, delving in the variables conventions further would not prove to be productive, yet following the naming conventions that we discussed above is good enough for your tests. If you would like to read more on the topic, I suggest reading the “Names” section of Effective Go and “Variable Names” section of the Go Code Review Comments wiki page.

Person looking into the void

Making things boring #

Probably you now:

Why do you bother me with these rules and conventions? go fmt takes care of my code, isn’t that enough?

I hear you and I get your point.

Here’s how I look at it: easy conventions to follow diminish entropy, which makes for simpler and predictable code. The less cognitive load we have to absorb when working with a piece of code – the better.

In other words: I like boring code. Boring code is good. I like code that will dully adhere to Go’s naming conventions, regardless if I find them appealing or not. Conventions are put in place to make our lives easier, by not having to think if it should be i or index, and knowing that conf will always mean configuration.

And I hope that you will find the boringness of such conventions liberating and empowering over time.