Testing in Go: Naming Conventions
Table of Contents
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.
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.
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:
- The base test function follows the format of
Test
+ the name of the function under test. For exampleTestCompare
, which tests theCompare
function. - 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 thePascalCase
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:
- 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, theAPI.Debug.Heap()
function is tested in theTestAPI_DebugHeap
test function. Similarly, theAPI.SetupTLSConfig
is tested inTestAPI_SetupTLSConfig
test function. - There are functions like
API.Agent.Services()
that require more specific tests. That’s why, for example there areTestAPI_AgentServices
andTestAPI_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
tolineCount
. Preferi
tosliceIndex
.
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.
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.