Какое именование файлов тестов принято в golang
Перейти к содержимому

Какое именование файлов тестов принято в golang

  • автор:

Записки программиста

На первый взгляд, модульные тесты в 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 умеет по-умному кэшировать результаты выполнения тестов. Иногда это кэширование хочется выключить. Сделать это можно так:

Чтобы оценить степень покрытия кода тестами, говорим:

Исключаем из отчета сгенерированный код:

Посмотрим статистику по функциям:

Посмотрим, какие строки кода остались непокрыты тестами:

В итоге откроется браузер, и в нем мы увидим следующее:

Покрытие кода тестами в языке Go

На отрицательный HP, пожалуй, тест можно и добавить. А вот из-за всевозможных недостижимых кусков кода я бы не стал переживать. На практике покрытие 90% строк кода — очень даже хороший результат.

Два слова об интеграционных тестах

В реальной жизни интеграционными тестами обычно называют те, которые явно не модульные (когда все что можно мокается, как было описано выше), и явно не системные (когда тестируется вся система целиком, честно установленная и настроенная на стенде), а где-то посередке. В принципе, это даже может быть и полноценный модульный тест, который просто очень долго выполняется. Таким тестам хочется присвоить какую-то метку и запускать их отдельно от всех остальных тестов.

Для достижения этого эффекта в Go нужно сделать две вещи. Во-первых, такие тесты нужно сложить в отдельном каталоге. Для определенности, пусть это будет каталог integration . Во-вторых, тесты в этом каталоге должны начинаться со строчки, содержащей метку:

Легко убедиться, что такие тесты не будут выполняться со всеми остальными.

Для их запуска следует воспользоваться командой:

Само собой разумеется, тэгов можно использовать много, и называться они могут как угодно.

Примечание: Если вы пишите код в GoLand, по умолчанию он не индексирует файлы под тэгами. Исправить это можно, введя используемые вами тэги через пробел в GoLand → Preferences… → Go → Build Tags & Vendoring → Custom tags.

Заключение

Как видите, при написании тестов в языке Go есть кое-какие нюансы.

Кое-что, конечно, в итоге осталось за кадром. Например, бенчмарки в Go пишутся почти аналогично тестам. Но о бенчмарках и профайлинге мы лучше поговорим в другой раз.

Полную версию исходников к посту вы найдете в этом репозитории на GitHub.

А чем вы пользуетесь при написании модульных тестов?

Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.

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.

Структура тестов Go, для RSpec-нутых

Для Ruby-разработчиков, которые знают что такое хорошие тесты на RSpec (остальных в Go ничего не смутит) и хотят писать тесты на Go соответствующим образом.

Для Go-разработчиков. На Go можно написать хорошие тесты, но почему-то я таких пока не видел ��. Почему-то даже в крупных и популярных пакетах используют не лучшие подходы.

Документация по тестированию на Go не очень богата и оставляет много вопросов (пока не получил опыта работы с RSpec, они у меня не возникали), ну что же, попытаемся на них ответить.

Немного предыстории

RNDSOFT как-то организовывала Ruby meetup , где я выступал с докладом "Тесты Go глазами Ruby-разработчика" и проводил параллели между тестами в Go и RSpec, рассказывая о трудностях, с которыми сталкиваешься, начиная писать тесты в Go, и как преодолеть их с наименьшей болью.

И вот сейчас у меня дошли руки и я хочу изложить мои мысли текстом, сконцентрировав внимание именно на структурировании тестов.

Что будет?

    Посмотрим, что говорит нам документация по поводу того, как нужно писать тесты. Обратим внимание на то, что документация не даёт нам всех ответов по структурированию тестов. Поймём, что для структурирования тестов в реальном проекте нам понадобится некое соглашение. Чтобы не изобретать велосипед, определим, какое соглашение можно взять за эталон и работать с ним. Разберём функционал, предоставляемый Go, с точки зрения RSpec (только стандартный пакет тестирования. Почему так, поговорим ниже ). Поймём, как в Go решить те же задачи, которые позволяет решить RSpec, но не выходя за парадигму Go.

Чего не будет?

Как можно скорее хочется спасти мебель от возгорания по вине тех, кто уже начал кричать что-то в духе:

    "Как вообще можно сравнивать RSpec и Go". "Привыкли писать на своём RSpec, так теперь везде вам подавай всё как там? Учитесь писать в парадигме Go". "Go строго типизированный, это вам не Ruby, в нём всё по-другому". "Go строго типизированный, в нём не нужны такие тесты, как на RSpec".

Выдохните, сосчитайте до десяти, заварите себе горячий напиток ��☕️��.

    попыток писать на Go, как на RSpec, сравнения специфический возможностей, сравнения особенностей написания тестов.

Я прекрасно понимаю, что языки разные и у них разные подходы. Но когда разработчик пишет, он должен (но не всегда это понимает) решать ряд стандартных задач. И вот именно о том, как взяв в руки Go, обеспечить тот необходимый минимум, с реализацией которого мы (Ruby-разработчики) справляемся, когда используем RSpec.

Почему не рассматриваем BDD фреймворки языка Go?

RSpec — это BDD фреймворк и было бы логично сравнивать его с BDD фреймворком в Go, например с пакетами Ginkgo или GoConvey , но делать мы этого не будем.

А дело собственно в том, что согласно опросу в телеграм канале Go-go! , который я проводил при подготовке к докладу, можно сказать, что описанные выше пакеты использует примерно 10%-15% разработчиков. Не говоря уже о том, что достичь тех же результатов можно и при помощи стандартного пакета, начиная с Go 1.7 уж точно.

К чему мы привыкли в Rspec?

Тестирование методов

В первую очередь стоит сказать, что это BDD фреймворк и поэтому имеет специфичную для этой парадигмы структуру написания тестов.

Основными элементами теста на Rspec являются блоки:

    describe — описывает тестируемый класс или метод, context — для описания состояний, окружения, контекста (когда пользователь авторизован, когда пользователь не авторизован), it — тело теста.

Когда мы описываем метод как .sum или ::sum, это означает, что мы тестируем метод класса (статический метод). Если мы описываем метод как #sum, значит мы тестируем метод инстанса (метод, вызываемый на объекте).

Тестирование функции

А вот тут ясности поменьше.

По сути можно сказать, что RSpec — это фреймворк для тестирования ООП приложений. И в рамках тестирования такого приложения, данный вопрос у вас не возникнет. Так например, шанс, что в Rails приложении вам понадобится сделать нечто подобное, стремится у нулю.

Но проблема лишь в том, что не ясно, как наиболее правильно описать такой тест.
RSpec по сути говорит нам, как наилучшим образом тестировать классы, но не запрещает нам использовать его для тестирования отдельных функций.

В таком случае, если мы тестируем некий скрипт, в котором есть отдельно стоящая функция sum, то мы можем написать тест как-то так и не переживать по этому поводу:

Тест не потерял в читаемости. Написать некий текст вместо имени тестируемого класса, вполне допустимо. В полноценном приложении (а не в скрипте), подобная ситуации — это редкость и исключительный случай. Так что сделаем маленькое исключение для него и не будем об этом жалеть.

Тестирование приватного метода

Да, подобный трюк лучше не исполнять. Но рассмотрим саму возможность.

Описание теста для приватного метода ничем не будет отличаться от публичного. Но тестировать придётся иначе, ведь Ruby не позволит просто так вызвать приватный метод. Но тут нам поможет метод send.

А как это в Go?

Прежде чем мы углубимся, я хочу сделать акцент на одном принципиальном для меня отличии RSpec и тестов в Go.

RSpec даёт каркас для написания тестов и набор рекомендаций, как всем этим пользоваться. Уже после сюда добавляются соглашения Better Specs, делающие тесты ещё прекраснее.

Go старается быть простым и однозначным (тот же вопрос с фигурными скобками решён весьма однозначно ��). Язык даёт нам много хорошего инструментария и простые, но строгие руководства, как с ним работать. И эти руководства не просто строгие, они не гибкие (в рамках официальной документации).

Собственно в этом и кроется ключевая проблема. RSpec с виду не так прост, но практически не оставляет вопросов. Есть описание в документации, есть соглашения, но помимо этого есть гибкость, позволяющая покрывать специфичные случаи. А Go однозначен и строг. И разработчик, оказавшись в ситуации строгости, но недосказанности, не может понять, как ему быть в тех или иных случаях, которые не покрывает документация. Само собой, сложности добавляет отсутствие достаточного опыта в языке и контакта с другими разработчиками Go, чтобы интуитивно понимать допустимые и недопустимые решения.

Тестирование функции

Тут всё на высоте. Всё просто и понятно. Идём читать документацию и

видим там следующий шаблон для написания тестов:

По сути, нам показали, как протестировать публичную (начинается с заглавной буквы) функцию.

Следовательно, если в нашем пакете есть публичная функция Sum, то тест для неё будет выглядеть вот так:

Собственно, это всё, что нам рассказывает документация касательно именования тестов.

Если в вашем приложении есть только функции, у вас нет структур, а если и есть, то они не содержат поведения, то описанного в документации вам достаточно, можете закрывать статью.

Тестирование приватной функции

Знаю-знаю, что тестировать приватные функции это не правильно, но в реальности случаются, пускай и редко, ситуации, где лучше сделать это, чем не сделать.

Но думаю, что Вы так не делаете. Вы же пишите идеальный код в идеальном приложении, спроектированном по всем канонам, а все разработчики, работавшие над ним, личной вскормлены дядюшкой Бобом.

Тестирование метода

Тоже не понять но как это делать. Нужно разбираться.

Тестирование в разных обстоятельствах (в разном окружении)

Тут ответов тоже нет. Нам говорят о технике табличного тестирования, которая хороша и закрывает часть проблем, но всё же не применима для задачи разделения контекста.

Тестирование приватной функции

Если написанное выше, касательно тестирования всего приватного, тебя не успокоило и душа всё ещё горит, можешь написать в комментах "Не тестируйте приватные функции".

Проблема тут в том, что шаблон именования теста, подразумевает, что название функции будет написано с большой буквы, то есть это будет публичная функция.

Поэтому, все варианты, которыми мы можем описать тест для приватной функции sum, буду нарушать соглашение из официальной документации.

Так написать конечно можно, но какой вариант выбрать и почему? Как объяснять это другим разработчикам. Ответы будут ниже .

Тестирование метода

Давайте вспомним нашу функцию Sum и то, как мы писали тест для неё:

Но что если Sum — это метод структуры? Можно предположить, что именование теста менять не стоит, и писать надо так же, как сказано в документации применительно к функциям. Это сработает, но глядя на именование теста, Вы не будете понимать, что тестируете. Подобная неоднозначность конечно не приятна, но с ней всё ещё можно мириться.

Трудности начинаются, когда в пакете присутствуют метод и функция с одинаковым именованием, как например в пакете viper .

К сожалению, в самом пакете viper не удалось найти тест, который бы показал, как решать эту проблему. Придётся выкручиваться самим.

Соглашение по именованию тестов из gotests — решение проблем

Ответы на поставленные выше вопросы и не только на них даёт пакет gotests . Этот пакет используется для автоматической генерации заготовок тестов, для абсолютно всего, что находится в вашем пакете.

Взглянув на то, как он работает, мы можем определить используемые им правила именования и позаимствовать их.

Правила выглядят следующим образом:

TestPubFunc(t *testing.T) Test_privateFunct(t *testing.T) TestPubStruct_PubMethod(t *testing.T) TestPubStruct_privateMethod(t *testing.T) Test_privateStruct_privateMethod(t *testing.T)

    Подобное именование решает все заявленные проблемы. Из названия теста понятно, что тестируется. Соглашение отражено в паке, у которого почти 4 тыс. звёзд на GitHub.

Как мне кажется, этих причин достаточно, чтобы внедрить эту практику именования в свою команду. Она будет понятна всем, а кому-то уже знакомой, и не нужно изобретать "велосипед".

Тестирование в разном контексте

Рассматривая, как подобное реализовано в RSpec, мы приводили вот такой пример:

Знатоки, вероятно, обратили внимание, что тех же результатов в Go можно добиться через табличное тестирование.

То есть вот так:

Единственное, чего тут не хватает, это ясности относительно того, а какой же случай мы тестируем в текущий момент.ю и какой тестовый набор провалился.

Документация даёт нам решение и предлагает использовать Subtests (подтесты).

Вот теперь мы будем понимать, какой тестовый набор провалился.

Вы скажете, что данная ситуация хорошо ложится на концепцию табличного тестирования, но есть и другие. И будете абсолютно правы.

Наиболее удобный и понятный пример, на мой взгляд, это ситуация с авторизованным и не авторизованным пользователем. Так же можно рассмотреть ситуации с разным состоянием конфигурации приложения, но давайте возьмём пример с пользователем.

Что делать, есть нам нужно проверить работу экшена контроллера для авторизованного и не авторизованного пользователя?

Напрямую документация не даёт ответа на этот вопрос, но почему бы нам не воспользоваться функциональностью Subtests? Не вижу причин отказывать себе в этом. Единственное, чего не хватает, это некоего соглашения о том, как описывать в подтестах различные ситуации. Нам не известно такое соглашение в Go, не беда, возьмём соглашение из Better Specs .

Мне кажется, что выглядит хорошо, не так ли?

В Go нет общепринятого стандарта именования тестов, так как официальная документация описывает лишь тестирование публичных функций, не уделяя внимание остальному, но это соглашение есть в пакете gotests . Изучите его и используйте, если не сам пакет, то хотя бы его подход.

Проблему тестирования в разном контексте прекрасно можно решить при помощи Subtests , нужно лишь добавить к ним хорошее соглашение по описанию тестируемых случаев, как это сделано в Better Specs .

К сожалению, в Go соглашения — это слабая сторона, во всяком случае в сравнении с экосистемой Ruby. Я полагаю, что локально команды решают этот вопрос, но не хватает именно общепринятых соглашений.

Отмечу, что разработать соглашение — это лишь малая часть работы, самое главное — это соглашение распространить в сообществе разработчиков.

По моему мнению, сообщество Go нуждается в большем внедрении стандартов и соглашений, поэтому призываю вас принимать активное участие в решении этой проблемы.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *