Различайте 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/

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