Naming tests in Golang
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.
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. : 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. would follow a similar approach:
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!
Test Naming Conventions in Golang
I’m trying to unit test a Go package for the first time, and I have a couple of errors in the same file.
However, all naming conventions seem to look like this func TestXxx(*testing.T) (from the testing package documentation). This would mean my testing file would look like this:
Which is obviously two functions of the same signature. What is the recommended method for handling this?
4 Answers 4
There are a few things to consider here:
Errors
Package-level exported error values are typically named Err followed by something, for instance ErrTimeout here. This is done so that clients of your package can do something like
To facilitate this, they are often created either with errors.New :
or with a custom, unexported type:
One advantage of the latter approach is that it cannot compare equal with a type from any other package.
In the case where you need some extra data inside the error, the name of the type ends in Error :
which, in contrast to the previous equality check, requires a type assertion:
In either case, it is important to document your code so that users of your package understand when they will be used and what functions might return them.
Tests
Tests are often named after the unit that they’re testing. In many cases, you won’t test error conditions separately, so TestError is not a name that should come up very often. The name of the test itself merely has to be unique, however, and is not constrained to match anything in the code under test in the same way that examples are. When you’re testing multiple conditions of a piece of code, it is often best to formulate the test as a Table Driven Test. That wiki page has some good examples, but to demonstrate error checking, you might do this:
If you do need a special test function for a unit that does something weird and doesn’t fit in the main test, you’d typically name it something descriptive like TestParseTimeout that includes both the unit and the behavior you’re testing.
Тестирование
Очень хорошим способом избежать ошибок является покрытие кода тестами. Обычно тесты размещают в отдельных файлах с названиями вида < Название пакета > _test.go . Внутри файла код тестов добавляют внутри функций следующего формата:
Внутри функции тестирования выполняется проверка каких-либо значений. Например, значений, возвращаемых функцией. Если значение соответствует ожидаемому значению, то ничего делать не нужно. Нормальный выход из функции тестирования означает, что тест пройден успешно. Если значение не соответствует ожидаемому значению, то через переменную t следует вызвать один из методов:
Записки программиста
На первый взгляд, модульные тесты в Go пишутся очень просто. Создаем файл с именем пакет_test.go , в нем объявляем функции с именами TestЧтоТестируем , говорим go test , ну и считай готово. Однако на деле все оказывается чуточку сложнее. Например, оказывается, что из коробки в языке нет ни ассертов, ни моков. А когда ты начинаешь генерировать моки, они внезапно начинают участвовать в подсчете покрытия кода тестами. В общем, давайте разберемся, как все устроено на самом деле.
Тестируемый код
Как и в статье про сериализацию, тестировать будем героев для какой-нибудь RPG. Поскольку на этот раз сериализовать мы ничего не будем, перепишем немного код, воспользовавшись типичной для Go реализацией типов-сумм через interface<> :
Примечание: Как альтернативный вариант, мы могли бы сказать, что Hero — это интерфейс, а Warrior и Mage являются совершенно независимыми структурами, реализующими данный интерфейс. Преимущество такого подхода заключается в том, что он позволяет избежать использования type switches, благодаря чему является более типобезопасным. Кроме того, при таком подходе в структурах используется меньше указателей. Минус подхода — некоторое распухание кода, связанное с тем, что для каждого экземпляра Hero требуется объявить все методы интерфейса, даже если реализация отдельно взятого метода у них одинакова и представляет собой обертку над одной и той же функцией. А чем больше кода, тем менее эффективно используются кэши CPU. Кроме того, поскольку Hero становится интерфейсом, все его методы будут вызываться через указатели, что ломает branch prediction. В общем и целом, у этих двух подходов есть свои слабые и сильные стороны.
Объявим немного вспомогательных методов:
// IsDead return true if hero has zero HP.
func ( h * Hero ) IsDead () bool <
return h . HP == 0
>
// IsWarrior return true if hero is a warrior.
func ( h * Hero ) IsWarrior () bool <
switch h . info . ( type ) <
case * WarriorInfo :
return true
default :
return false
>
>
// IsMage returns true if hero is a mage.
func ( h * Hero ) IsMage () bool <
switch h . info . ( type ) <
case * MageInfo :
return true
default :
return false
>
>
Реализуем метод Attack :
Атаковать можно не только других героев, но также мобов, предметы интерьера, и так далее. Поэтому аргументом методу передается не Hero , а интерфейс CanTakeDamage :
Число, возвращаемое TakeDamage , представляет собой урон, наносимый атакующему. Например, маг может иметь защитное поле, а огненный элементаль жжется. В настоящей игре возможных эффектов, накладываемых на атакующего, конечно, должно быть куда больше. Но для простоты ограничимся только одним возможным эффектом — получение ответного урона при атаке.
func ( h * Hero ) doMageAttack ( enemy CanTakeDamage ) <
info := h . info . ( * MageInfo )
if info . Mana < = 5 <
// there is no enough mana
return
>
if len ( info . Spellbook ) == 0 <
// there are no known spells
return
>
info . Mana -= 5
h . TakeDamage ( enemy . TakeDamage ( 20 ))
>
Маги могут атаковать только при наличии маны и изученных заклинаний. Для простоты будем считать, что все заклинания наносят 20 очков урона и стоят 5 единиц маны.
Воин, вооруженный луком, может атаковать только при наличии стрел. Мечем можно атаковать всегда. Стрелы всегда наносят 12 единиц урона, а меч — 8 единиц.
// TakeDamage takes the damage and returns the damage
// an attacker should take (e.g. because of applied spells)
func ( h * Hero ) TakeDamage ( num int ) int <
h . HP -= num
if h . HP < 0 <
h . HP = 0
>
if ( h . IsMage ()) <
// all mages are always protected
return num / 10
> else <
return 0
>
>
Наконец, реализуем TakeDamage для героев. Для простоты будем считать, что вокруг магов всегда есть волшебный щит, возвращающий атакующему 10% от нанесенного урона. Воины лишены каких-либо особых эффектов. Для баланса в будущих версиях игры, вероятно, им стоит добавить броню, благодаря которой воины принимают только часть урона.
Покрываем код тестами
Ну что же, думаю, логика получилась достаточно сложной, чтобы ее захотелось покрыть тестами. В предположении, что приведенный выше код находился в файле heroes.go , создадим файл с тестами под именем heroes_test.go .
Начнем со совсем простого теста:
func TestHeroIsDead ( t * testing . T ) <
// t.Skip()
t . Parallel ()
h := heroMage ()
require . False ( t , h . IsDead ())
h . HP = 0
require . True ( t , h . IsDead ())
>
Как видите, ассерты было решено взять из пакета testify/require . Помимо проверок на True и False , также можно проверять Equal , NotEqual , Nil , NotNil , Zero , и много чего еще. Полную документацию вы найдете на godoc.org.
Строчка t.Parallel() говорит, что тест можно запускать параллельно с другими параллельными тестами. Если тест хочется на время выключить, можно написать t.Skip() . Главное не вмержить скипнутые тесты в мастер. Как по мне, уж лучше совсем выкинуть тест, чем держать куски мертвого кода.
Поскольку сейчас мы пишем модульные тесты, настоящим должен быть только код тестируемых функций, а все остальное необходимо замокать. Чтобы не писать моки руками, было решено воспользоваться minimock. Пользоваться им очень просто. Допустим, нам нужен мок с интерфейсом CanTakeDamage . Исправим код таким образом:
// на самом деле это одна строка, но здесь она не влезла:
//go:generate minimock -i github.com/afiskon/golang-unit-testing/⏎
// heroes.CanTakeDamage -o . -s _mock.go
type CanTakeDamage interface <
TakeDamage ( num int ) int
>
Будем получен файл can_take_damage_mock.go с нашим моком. Пример его использования:
В результате тест проверит, что TakeDamage вызывается ровно один раз и именно с заданным аргументом. Если не сказать ExpectOnce , а метод, тем не менее, будет вызван, то тест не пройдет. Можно также указать конкретную реализацию интересующего метода:
Или, например, сказать Expect вместо ExpectOnce и узнать, сколько раз вызывался метод, посмотрев на TakeDamageCounter . Тут проще всего ориентироваться по автокомплиту в IDE. Я лично в настоящее время пользуюсь GoLand от компании JetBrains.
Вообще, IDE очень сильно упрощает работу с тестами. Можно быстро переходить к упавшему тесту, запускать только заданный тест, в том числе под отладчиком, и так далее.
Мной было написано еще несколько тестов. Однако они принципиально ничем не отличаются от приведенных выше, поэтому двигаемся дальше.
Из консоли тесты запускаются как-то так:
Go умеет по-умному кэшировать результаты выполнения тестов. Иногда это кэширование хочется выключить. Сделать это можно так:
Чтобы оценить степень покрытия кода тестами, говорим:
Исключаем из отчета сгенерированный код:
Посмотрим статистику по функциям:
Посмотрим, какие строки кода остались непокрыты тестами:
В итоге откроется браузер, и в нем мы увидим следующее:

На отрицательный HP, пожалуй, тест можно и добавить. А вот из-за всевозможных недостижимых кусков кода я бы не стал переживать. На практике покрытие 90% строк кода — очень даже хороший результат.
Два слова об интеграционных тестах
В реальной жизни интеграционными тестами обычно называют те, которые явно не модульные (когда все что можно мокается, как было описано выше), и явно не системные (когда тестируется вся система целиком, честно установленная и настроенная на стенде), а где-то посередке. В принципе, это даже может быть и полноценный модульный тест, который просто очень долго выполняется. Таким тестам хочется присвоить какую-то метку и запускать их отдельно от всех остальных тестов.
Для достижения этого эффекта в Go нужно сделать две вещи. Во-первых, такие тесты нужно сложить в отдельном каталоге. Для определенности, пусть это будет каталог integration . Во-вторых, тесты в этом каталоге должны начинаться со строчки, содержащей метку:
Легко убедиться, что такие тесты не будут выполняться со всеми остальными.
Для их запуска следует воспользоваться командой:
Само собой разумеется, тэгов можно использовать много, и называться они могут как угодно.
Примечание: Если вы пишите код в GoLand, по умолчанию он не индексирует файлы под тэгами. Исправить это можно, введя используемые вами тэги через пробел в GoLand → Preferences… → Go → Build Tags & Vendoring → Custom tags.
Заключение
Как видите, при написании тестов в языке Go есть кое-какие нюансы.
Кое-что, конечно, в итоге осталось за кадром. Например, бенчмарки в Go пишутся почти аналогично тестам. Но о бенчмарках и профайлинге мы лучше поговорим в другой раз.
Полную версию исходников к посту вы найдете в этом репозитории на GitHub.
А чем вы пользуетесь при написании модульных тестов?
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.