Naming tests in Golang

Diogo Mateus
GetGround
Published in
6 min readAug 24, 2021

Written by Diogo, Senior Backend Engineer at GetGround

GetGround’s mission is to make assets more transparent, trustworthy and accessible. We are doing this by bringing the world’s assets online, starting with residential property. We believe increased access to asset ownership will enable a fairer, more productive world.

We’re constantly looking for ways of improving the efficacy of our tests as it is a fundamental practice to software quality and a way to pursue excellence.

One of our recent topics of discussion was the test naming strategy. Even though it doesn’t directly affect the tests or the coverage, a good naming strategy brings many benefits to the table. In this blog post I’ll explore test naming strategies and why they are important. All of this will be done with a focus in Golang, our main server side language, but first let’s start with the why.

Why do we care about effective test names?

  • Tests act as documentation. By taking a look at the implementation you’ll see how the system works, but by looking at the tests you’ll see what it does. You should be able to quickly scan through all the test names and understand the system’s requirements.
  • Good test names are abstracted from the implementation and resist refactorings.
  • A structured naming strategy will add consistency to the codebase. Developers will know exactly what to expect.

Naming tests in Go

Golang doesn’t offer many recommendations on this topic.

func TestXxx(*testing.T) — where Xxx does not start with a lowercase letter. The function name serves to identify the test routine. — Go’s testing documentation

This is all you’ll find in Go’s testing documentation when it comes to naming tests. If you couple it with the “Shorter is better” practice, it will result in names like these:

  • func TestAbs(t *testing.T)
  • func TestSplit(t *testing.T)
  • func TestAPI_AgentServices(t *testing.T)
  • func TestService_CreateUser(t *testing.T)
  • func TestService_CreateUser_Success(t *testing.T)
  • func TestCreateUserBadRequest(t *testing.T)

There are a few issues with this approach:

  • Many of these names refer to success, but success is ambiguous. What does it really mean? What’s actually being tested? Let’s take an example of an endpoint that creates a user. Does success mean that the user was added to the database? Does it mean that the endpoint returns the expected json response? Does it mean both?
  • As mentioned above, success may mean testing more than one thing at the same time, something you want to avoid when writing unit tests.
  • What about TestCreateUserBadRequest? Is it testing a malformed json, an empty body, a missing parameter, a wrong value type, all of them?
  • These approaches may also promote mindsets like: “okay I tested the main scenario, this is enough, let’s move on”.
  • Finally, it may be putting too much focus on testing functions instead of testing requirements. This creates tests that are hard to maintain: we can’t refactor easily because implementation details are exposed to tests.

Naming conventions

Let’s take a look at some of the most established naming strategies for tests.

Roy Osherove’s naming strategy

It follows the [UnitOfWork_StateUnderTest_ExpectedBehaviour] pattern where: UnitOfWork represents a single method, a class or multiple classes; StateUnderTest represents the inputs or conditions being tested and ExpectedBehaviour represents the output or result.

Let’s go back to the create user endpoint example and use this strategy to generate a Golang test name: Test_CreateUser_EmptyBody_BadRequestErrorThrown.

Because this naming strategy focus on the SUT, inputs and output, it expresses a very clear requirement.

Test name should be presented as a statement or fact of life that expresses workflows and outputs — Naming standards for unit tests

The main criticism against this approach is that it doesn’t resist refactors (e.g. when the UnitOfWork changes). Additionally, it may be difficult to decipher for those who aren’t used to it.

The Plain English strategy

This strategy is defended by Vladimir Khorikov in his blog post: You are naming your tests wrong!.

It basically states that the test names should be written in plain english and shouldn’t follow any rigid policies. Our example would become something like: Create_User_With_Empty_Body_Is_Invalid or Cant_Create_User_On_Empty_Body. It benefits from being flexible, easy to read and because it doesn’t mention the SUT, it also handles refactors well.

Let’s take a look at some other examples to get a better idea of it:

  • Delivery_With_A_Past_Date_Is_Invalid
  • Add_Credit_Updates_The_Customer_Balance
  • Purchase_Without_Funds_Is_Not_Possible

My criticism against this one is that the lack of structure may allow for very different approaches within one codebase. It may also fall on the success issue I mentioned earlier where tests are named Create_User or Add_Discount, which doesn’t provide much clarity on what’s being tested.

The Should strategy

This strategy follows the Should_ExpectedBehaviour_When_StateUnderTest pattern. It’s similar to Roy Osherove’s naming strategy but doesn’t include the SUT in the name, which makes it more behaviour-oriented and resistant to refactors. It is also more human readable.

Example: Should_Return_BadRequest_Error_When_Body_Is_Empty

But a test should represent a fact about the SUT, not a desire, and so the word “should” has no place in it.

The Behaviours strategy

This one is basically the Should strategy without the “Should” word: ExpectedBehaviour_When_StateUnderTest

Example: Returns_BadRequest_Error_When_Body_Is_Empty. Here we aren’t manifesting a wish about the SUT but making a statement about it. It’s more direct and less verbose.

This approach is structured, descriptive and direct, but lacks the flexibility of Plain English.

Structured vs Unstructured

Vladimir Khorikov advocates for the Plain English names strategy, which is the most flexible approach. The names are more human readable, they describe the behaviour without constraints and may even directly reflect the story’s Acceptance Criteria.

On the other hand, a structured approach like the Behaviours strategy is less chaotic, the programmer knows exactly what to expect from it.

In the end it’s a choice between chaos vs order, human vs machine. Both have their pros and cons and are equally valid.

Because I enjoy the predictability of structure, I’ll expand a little more on the Behaviours strategy.

How to write names based on behaviours?

Start by writing a checklist of how the SUT should behave. Using the create user endpoint example, the checklist could be:

  • Returns bad request when the body is empty
  • Returns bad request when the json is invalid
  • Returns bad request when the user is invalid
  • Returns conflict when the user already exists
  • Returns success when the user is created

By following this approach you’re describing what the SUT does and the test will reflect that. It will also force you to test one thing at a time as you don’t want more than one condition in the requirement.

The final step is to turn these behaviours into test names. There are a few options when it comes to naming them in Golang:

  • Test_CreateUser_Returns_BadRequest_When_The_Body_Is_Empty: this one has the SUT attached to the beginning of the name. It won’t be very resistant to refactorings but offers clarity in case there are other SUTs in the same package.
  • Test_Returns_BadRequest_When_The_Body_Is_Empty: this approach is nicer but will require that the SUT is alone in the package. An alternative is to use Suites from the testify package. The suite will isolate the tests and provide helpers like setup, teardown, etc.
  • TestReturnsBadRequestWhenTheBodyIsEmpty: for those who don’t like underscores.
  • Subtests: t.Run("returns bad request when the body is empty"), func(t *Testing) { }. More elegant than the others, but because it is a subtest, it has its limitations.
  • Table-driven tests would follow a similar approach:
{
name: "returns bad request when the body is empty",
haveReqBody: `{}`,
wantResCode: 400,
}

How to come up with the names

When writing the test names, start by thinking about what you want as a goal. Don’t focus on implementation details that can easily change. Focus one one outcome or a consequence, preferably using business terms when possible.

It helps to start with a verb like: Creates, Returns, Throws, Converts, Updates, Calls. The rest comes naturally.

Here are a few more examples:

  • Calls_Storage_When_Saving_User
  • Returns_List_Of_Users_From_Storage
  • Sends_Customer_Email
  • Lists_Sent_Messages

⚠️ If I’m finding it difficult to come up with a name for test, that most likely means that something is wrong: I may be trying to test more than one thing at the same time or maybe the SUT has too many responsibilities.

Easy to maintain

Once the behaviours are defined and the tests are written, the only reason to update them is when the requirements change.

The developers will also be free to refactor the implementation however they like, because the tests and the implementations aren’t coupled.

Concluding remarks

I believe tests have potential to be the best source of documentation and that starts with the naming strategy. There are many possible approaches, each having their own pros and cons. Picking one should be a team effort.

At GetGround we are continually pushing ourselves to be better programmers and making improvements on our coding practices, either through Backend Guild meetings, RFC’s, or pairing sessions. If you think you might enjoy being part of a team like ours, connect with me on LinkedIn as we’re hiring!

Recent news

Careers at GetGround

--

--