Хотя язык программирования Go идёт в комплекте со встроенным тестовым фреймворком, мне сложно себе представить написание всего того количества тестов, что я написал, без testify. В этой заметке я расскажу про несколько маленьких неочевидных трюков, которым я научился в процессе.

В testify есть два основных пакета с проверками — assert и require. Набор проверок в них идентичен, но фейл require-проверки означает прерывание выполнения теста, а assert-проверки — нет.
Когда мы пишем тест, мы хотим, чтобы неудачный запуск выдал нам как можно больше информации о текущем (неправильном) поведении программы. Но если у нас есть череда проверок с require, неудачный запуск сообщит нам только о первом несоответствии.
Поэтому имеет смысл пользоваться require-проверками только если дальнейшее выполнение теста в случае невыполнения условия лишено смысла. Например, когда мы проверяем отсутствие ошибки, или валидируем длину списка, в который полезем дальше по коду теста.
Также стоит быть осторожнее при использовании горутин в тестах. require-проверки производятся через
Очевидно, для реализации практически любого мыслимого теста (кроме паникующего) достаточно одной функции
Аналогично, тест по умолчанию считается упавшим в случае паники, но использование
Этот совет может звучать совсем уж очевидным, но плохо структурированные тесты — проблема, встречающаяся повсеместно.
Suite собирает тесты, объединённые общими компонентами и тестовыми данными.
Методы сюиты проверяют разные, независимые друг от друга сценарии использования этих компонент и сущностей. Они могут запускаться в произвольном порядке.
Секции t.Run() разделяют сценарии на последовательные логические части.
При этом возможностью двухуровнево структурировать тесты внутри сюиты легко злоупотребить — этого тоже следует избегать. Однажды я наткнулся на сюиту в 2 000 строк кода — и оказалось, что это маленький тест, который я написал несколько лет назад и назвал слишком общими словами, спровоцировав коллег одного за другим добавлять туда новые тесты для совершенно несвязанных фичей. Зато каждый тест был в отдельном методе.
Иногда в тестах бывает необходимо вызвать какой-то метод у объекта, который в обычном продакшне не нужен (или даже опасен). Например, если у нас есть компонент с кэширующим слоем и мы хотим после изменения каких-то тестовых данных этот кэш сбросить.
В таком случае удобно положить реализацию этого метода и тестовый интерфейс в отдельный файл с именем
Однако, в таком случае наш тестовый тип и метод будут доступны только в тестах в этой же папке: в пакетах
Гораздо универсальнее и удобнее положить их в обычный
При этом нужно будет начать прокидывать
Также в файлы с
А какие best practices написания тестов на Go используете вы? Поделитесь в комментариях!

Различайте assert и require
В testify есть два основных пакета с проверками — assert и require. Набор проверок в них идентичен, но фейл require-проверки означает прерывание выполнения теста, а assert-проверки — нет.
Когда мы пишем тест, мы хотим, чтобы неудачный запуск выдал нам как можно больше информации о текущем (неправильном) поведении программы. Но если у нас есть череда проверок с require, неудачный запуск сообщит нам только о первом несоответствии.
func TestBehavior(t *testing.T) { ... price, err := priceManager.GetPrice(ctx, productID) require.NoError(t, err) require.Equal(t, 300, price.Amount) require.Equal(t, money.USD, price.Currency) } /* === RUN TestBehavior temp_test.go:21: Error Trace: behavior_test.go:21 Error: Not equal: expected: 300 actual : 42 // but is it at least bucks? Test: TestBehavior --- FAIL: TestBehavior (0.00s) Expected :300 Actual :42 */
Поэтому имеет смысл пользоваться require-проверками только если дальнейшее выполнение теста в случае невыполнения условия лишено смысла. Например, когда мы проверяем отсутствие ошибки, или валидируем длину списка, в который полезем дальше по коду теста.
func TestBehavior(t *testing.T) { ... price, err := priceManager.GetPrice(ctx, productID) require.NoError(t, err) assert.Equal(t, 300, price.Amount) assert.Equal(t, money.USD, price.Currency) } /* === RUN TestBehavior behavior_test.go:22: Error Trace: behavior_test.go:22 Error: Not equal: expected: 300 actual : 42 Test: TestBehavior behavior_test.go:23: Error Trace: behavior_test.go:23 Error: Not equal: expected: USD actual : RUB Test: TestBehavior --- FAIL: TestBehavior (0.00s) */
Также стоит быть осторожнее при использовании горутин в тестах. require-проверки производятся через
runtime.goexit(), так что они сработают ожидаемым образом только в основной горутине.Используйте подходящие проверки вместо универсальных
Очевидно, для реализации практически любого мыслимого теста (кроме паникующего) достаточно одной функции
assert.True(). Тем не менее, в testify есть уйма проверок для разных случаев жизни. Использование более подходящей проверки делает сообщения об ошибках более читаемыми и экономит код.❌ require.Nil(t, err) ✅ require.NoError(t, err) ❌ assert.Equal(t, 300.0, float64(price.Amount)) ✅ assert.EqualValues(t, 300.0, price.Amount) ❌ assert.Equal(t, 0, len(result.Errors)) ✅ assert.Empty(t, result.Errors) ❌ require.Equal(t, len(expected), len(result) sort.Slice(expected, ...) sort.Slice(result, ...) for i := range result { assert.Equal(t, expected[i], result[i]) } ✅ assert.ElementsMatch(t, expected, result)
Аналогично, тест по умолчанию считается упавшим в случае паники, но использование
assert.NotPanics() помогает будущему читателю теста понять, что вы проверяете именно её отсутствие.Структурируйте тесты с помощью Suite и t.Run()
Этот совет может звучать совсем уж очевидным, но плохо структурированные тесты — проблема, встречающаяся повсеместно.
Suite собирает тесты, объединённые общими компонентами и тестовыми данными.
Методы сюиты проверяют разные, независимые друг от друга сценарии использования этих компонент и сущностей. Они могут запускаться в произвольном порядке.
Секции t.Run() разделяют сценарии на последовательные логические части.
При этом возможностью двухуровнево структурировать тесты внутри сюиты легко злоупотребить — этого тоже следует избегать. Однажды я наткнулся на сюиту в 2 000 строк кода — и оказалось, что это маленький тест, который я написал несколько лет назад и назвал слишком общими словами, спровоцировав коллег одного за другим добавлять туда новые тесты для совершенно несвязанных фичей. Зато каждый тест был в отдельном методе.
Прячьте вспомогательные методы за //go:build
Иногда в тестах бывает необходимо вызвать какой-то метод у объекта, который в обычном продакшне не нужен (или даже опасен). Например, если у нас есть компонент с кэширующим слоем и мы хотим после изменения каких-то тестовых данных этот кэш сбросить.
В таком случае удобно положить реализацию этого метода и тестовый интерфейс в отдельный файл с именем
*_test.go. Так мы выставляем наружу нужные для тестирования методы, но не засоряем публичный интерфейс пакета.package mypackage type TestManager interface { Manager ClearCache(ctx context.Context) error } // Поскольку мы в том же пакете, мы можем обращаться // к приватным структурам и даже добавлять новые методы. func (m *manager) ClearCache(ctx context.Context) error { return m.myStuffCache.Clear(ctx) }
Однако, в таком случае наш тестовый тип и метод будут доступны только в тестах в этой же папке: в пакетах
mypackage и mypackage_test. Это довольно серьёзное ограничение. (Также для таких ситуаций предусмотрен довольно хитрый механизм сборки, способный значительно замедлить покоммитные тесты.)Гораздо универсальнее и удобнее положить их в обычный
.go-файл и выключить его компиляцию клаузой //go:build testmode.//go:build testmode package mypackage type TestManager interface { Manager ClearCache(ctx context.Context) error } func (m *manager) ClearCache(ctx context.Context) error { return m.myStuffCache.Clear(ctx) }
При этом нужно будет начать прокидывать
-tags testmode при прогоне тестов и сделать отдельную джобу, проверяющую сборку бинарей без этого тега (если у вас в принципе есть CI/CD).Также в файлы с
//go:build testmode можно складывать тестовые утилиты, свои кастомные сюиты со вспомогательными методами и так далее.А какие best practices написания тестов на Go используете вы? Поделитесь в комментариях!
