Четыре простых лайфхака при написании тестов на Go + testify

от автора

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


Различайте 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 используете вы? Поделитесь в комментариях!


ссылка на оригинал статьи https://habr.com/ru/company/joom/blog/666440/


Комментарии

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

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