Существует множество видов функциональных и нефункциональных тестов:
- Юнит-тесты — тестируют минимальную часть функциональности (функцию или методы) в полной изоляции от внешних зависимостей. По сути, тестируются отдельные небольшие кусочки кода.
- Интеграционные тесты — тестируют взаимодействие нескольких крупных частей приложения, например систем оформления заказов и оплаты.
- End-to-end-тесты — тестируют работоспособность всей системы.
- Мутационные тесты — тестируют код на устойчивость к случайным изменениям.
- Нагрузочные тесты — используются для определения максимальной нагрузки, которую система способна выдержать с допустимым уровнем деградации.
Note
Напомним, что функциональные тесты проверяют работоспособность разработанного кода, то есть соответствие функциональным требованиям. Нефункциональные (например, нагрузочные) определяют соответствие кода требованиям надёжности, качества, сопровождения и т. п.
Сконцентрируемся на юнит-тестах, потому что:
- Это самый простой для анализа вид тестов.
- При правильном подходе к тестированию таких тестов в кодовой базе будет большинство.
- Техники и приёмы, которые будут продемонстрированы на их примере, можно использовать и в других типах функциональных тестов.
Инструментарий языка Go, поставляемый вместе с компилятором, включает в себя обширные средства тестирования. Будем рассматривать именно их.
Где размещаются юнит-тесты в Go?
В Go все тесты должны располагаться в файлах с суффиксом _test.go
: например, user_test.go
. Юнит-тесты принято располагать рядом с тестируемым кодом. Файлы user.go
и user_test.go
обычно лежат в одной и той же директории.
Файлы *_test.go
не участвуют в компиляции финальной сборки проекта, поэтому можно не бояться импортировать в них большие библиотеки вроде stretchr/testify
.
Тем не менее при компиляции тестов, как и основного кода, запрещены циклические импорты. Однако при написании тестов они могут возникать часто, поскольку в тестах может понадобиться код, который зависит от тестируемого кода. В таком случае тесты будут зависеть от тестируемого кода и, следовательно, импортировать сами себя. Поэтому для тестов сделали единственное исключение из правила «одна директория — один пакет». Тестовые файлы могут располагаться в пакете с суффиксом _test
— и этой возможностью лучше пользоваться.
Также в пакете _test
стоит располагать код, нужный исключительно для тестов. Допустим, надо сделать приватный тип публичным или добавить класс вспомогательных методов. Вспомогательный файл (часто его называют harness_test.go
или common_test
) может выглядеть так:
Тесты в Go
Теперь, когда вы знаете, где и как хранить тестовый код, поговорим о том, как именно писать тесты на Go.
В Go все тесты — это функции вида:
Префикс Test
обязателен. В качестве Xxx
обычно указывают название тестируемой функции. У каждой тестируемой функции может быть несколько тестов, и тогда нужно указать дополнительную информацию по конкретному тесту.
Для примера протестируем функцию Add
, которая должна сложить два числа при условии, что они положительные. Если одно или оба числа равны нулю, функция должна вернуть ошибку.
Файл add.go
:
Файл add_test.go
:
Объект *testing.T
предоставляет доступ к нескольким базовым методам:
Error
,Errorf
— записывает сообщение вerror
-лог и помечает тест как непройденный. Исполнение теста продолжается.Fatal
,Fatalf
— делает то же самое, но исполнение теста немедленно завершается. Этот метод часто используется в рабочих проектах при обработке ошибок. Очень удобен при отладке, когда тестируется какой-то конкретный участок кода.Skip
,Skipf
— позволяет пропустить тест с сообщением. Используется, когда окружение для теста не задано. Типичный сценарий — прогон интеграционных тестов с внешним сервисом только на CI, где к нему есть доступы.Log
,Logf
— позволяет выводить лог-сообщения внутри теста. Преимущество перед методами пакетаfmt
в том, что из лога сразу видно, к какому тесту относится сообщение.Run(name string, testf func(t *testing.T) )
— запускает функцию в качестве теста, что удобно при выполнении нескольких запусков теста, например, с разными именами.
go test
Теперь разберём запуск написанных тестов. Для этого в экосистеме Go используется стандартная утилита go test. Она позволяет запускать тесты следующими способами:
- Тесты на основании положения кода в директории. Чтобы запустить все тесты в директории, достаточно перейти в неё и выполнить команду
go test
илиgo test -v
. Флаг-v
перенаправляет наstdout
всё, что тесты логируют вstdout
иstderr
. - Тесты во всех поддиректориях текущей директории:
go test ./...
- Все тесты пакета. Чтобы запустить все тесты в пакете, утилите
go test
надо передать пути импорта этих пакетов, разделённые пробелами. Например:go test math github.com/username/packagename github.com/username/packagename2
. - Тесты, подходящие под регулярное выражение. Также есть возможность протестировать некоторое подмножество тестов пакета. Для этого используется флаг
-run
утилитыgo test
.
Например, если нужно протестировать все тест-кейсы с префиксом TestFunc
в пакете github.com/ytuser/ytpackage
, вызов команды go test
будет выглядеть так:
В качестве аргумента передаётся регулярное выражение, под которое должны подходить названия тестов.
Кеширование тестов
Если повторно запустим команду go test <PACKAGE_NAME>
, то увидим, что вывод команды изменился. В случае с пакетом math
получим:
ok math (cached)
Дело в том, что в режиме тестирования пакета go test
кеширует результат прогона тестов и, если код и тесты не были изменены, использует закешированный результат.
Отключить кеширование можно двумя способами:
- Передать флаг
-count 1
, который определяет, сколько раз нужно запустить каждый тест (по умолчанию — один). Соответственно,-count 1
не изменяет количество запусков — если сравнивать со значением по умолчанию, — но выключает кеширование. - Запустить команду
go test clear
, очищающую кеш.
Дополнительные настройки тестирования
-cpu 1,2,4
— позволяет прогнать все тесты несколько раз с использованием разного количества потоков. Пригодится, если нужно протестировать параллельный код и убедиться, что на машинах с разным количеством ядер он будет работать корректно.-list regexp
— вместо того чтобы запускать тесты,go test
выведет в консоль имена тестов, подходящих под переданное регулярное выражение.-parallel n
— позволяет параллельно выполнять тесты, которые в теле вызываютt.Parallel
.-run regexp
— позволяет запускать конкретные тесты.-short
— если этот флаг передан, тоt.Short() == true
. В этом случае можно либо пропустить длительные тесты, либо урезать их функционал.-v
— подробное логирование. Даже в случае успешного прохождения тестов весь их лог будет выведен в консоль.
Покрытие кода
Одна из важнейших метрик качества кода — степень покрытия тестами (test coverage). В Go для вычисления этой метрики используется флаг -cover
утилиты go test
, подробнее о котором можно почитать в официальном блоге Go.
Например, если вы хотите узнать степень покрытия тестами пакета math
из стандартной библиотеки, надо вызвать команду go test math -cover
:
ok math 0.003s coverage: 86.8% of statements
Определим покрытие тестами функции Add
из примера выше:
% go test -cover
PASS
coverage: 80.0% of statements
ok tests_06 0.233s
Но знания одной только метрики зачастую не хватает, и нужно выяснить, какие именно строки кода не были задействованы при прогоне тестов. Этот функционал тоже идёт «из коробки» в виде флага -coverprofile
утилиты go test
и специальной утилиты go cover для анализа профиля покрытия тестами.
Итак, если вы хотите узнать, какой именно код пакета math
не был покрыт тестами, надо сделать следующее:
- Запустить на нём утилиту
go test
и сохранить файл профиля покрытия тестами. Путь к файлу с профилем — это значение флага-coverprofile
. В данном случае сохраним его в файлcoverage.out
текущей директории:
- Проанализировать полученный файл утилитой
cover
. Например, по собранному профилю можно получить HTML-представление исходного кода с дополнительной разметкой, связанной с покрытием тестами:
После выполнения автоматически запустится браузер, где будет отображена информация по покрытию.
Удобное тестирование
Можно тестировать код, используя только *testing.T
, но он не предоставляет доступ к функциям вроде проверки на равенство, проверки на возврат ошибки или проверки на наличие паники при вызове переданного колбэка.
Этот пробел заполняют сторонние библиотеки, среди которых самая популярная — testify. Следует отметить, что они не являются заменой стандартного testing
, а расширяют и дополняют его.
- testify — это швейцарский нож в мире тестирования Go-кода. Поэтому поговорим про часто используемые пакеты из этого репозитория отдельно.
- assert — пакет, в котором собрано множество удобных функций с говорящими названиями: вроде
assert.Equal
илиassert.Nil
. Предназначен для использования внутри обычных Go-тестов видаfunc TestXxx(t *testing.T)
(про необычные поговорим, когда доберёмся до suite). - require — то же самое, что assert, но в случае, если проверки из этого пакета падают, выполнение теста останавливается. То есть внутри используется
Fatal
вместоError
. - suite — пакет, который вводит концепцию тест-сьюта (test suite). Если вы работали с тестами на Java или Python, то, скорее всего, она вам знакома.
Сьют — это объект, содержащий сами тесты в виде методов, а также некоторый набор переменных, доступный всем тестам. Кроме того, сьют даёт возможность определить код, который будет вызван перед и/или после исполнения теста. Эта функция полезна, например, для очистки базы данных между тестами (в случае интеграционных тестов).
Типичный пример работы со suite из официальной документации выглядит так:
Паттерны тестирования
Есть несколько рекомендаций, как стоит и не стоит писать тесты. Они не специфичны для Go, поэтому их можно применить для тестирования на любом языке.
Каждый тест должен тестировать что-то одно
Например, если вы тестируете функцию func Divide(a, b int) (int, error)
, не стоит писать такой код:
Он будет падать при любой проблеме в тестируемой функции. Лучше разбить его на три теста (или три подтеста внутри этого теста), каждый из которых тестирует свой специфический сценарий:
Тесты не должны зависеть друг от друга
Если один тест опирается на глобальное состояние, устанавливаемое другими тестами, — это проблема. Тестовая архитектура такого типа приводит к неприятным ошибкам, когда локально тесты проходят, а на CI/CD иногда падают, потому что время от времени порядок выполнения тестов меняется.
Эта же рекомендация относится к тестам внутри одного сьюта. При написании каждого теста нужно исходить из того, что ему на вход передаётся состояние после вызова подготовительного метода SetupTest
.
Результат работы теста — это не лог
При написании теста стоит считать, что при успешном прохождении теста никто на его логи смотреть не будет. Все контракты, которые фиксирует тест, должны быть прописаны в виде проверок. В этом случае можно безопасно гонять тесты на CI/CD без участия человека.
Table-driven-тесты
В примерах выше мы видели, что тесты содержат много повторяющегося кода, и это неспроста. Существует паттерн тестирования, который называется table-driven, или, говоря по-русски, «табличное тестирование». Он реализуется не только в Go, но и в других языках программирования.
Суть паттерна в отделении тестовых данных от выполнения самих тестов. Для коротких тестов паттерн может показаться избыточным, однако он позволяет эффективно организовать и расширять тесты, если тестовых наборов требуется несколько.
Note
В Go есть пакет генерации шаблонов тестов именно в стиле table-driven, поэтому для рассмотрения примера будем использовать генерацию кода.
Если вы используете GoLand или VS Code с плагинами, то инструменты генерации уже встроены в IDE. Если нет, то потребуется немного дополнительных действий.
Установите пакет github.com/cweill/gotests
:
$ go get -u github.com/cweill/gotests/...
Вернёмся к нашей функции Add
. Сгенерируем для неё заготовку теста:
В файл math_test.go
добавился следующий код. Мы добавили к нему комментарии, чтобы было понятнее, что он делает.
Добавим тестовые случаи в место, указанное в //TODO
:
И запустим тесты.
Удобство такого подхода в том, что не требуется добавлять вызов и проверки, чтобы добавить ещё несколько случаев. Достаточно описать аргументы и желаемое поведение.
Ключевые мысли
- В Go инструменты тестирования обеспечивают всё необходимое для проведения юнит-тестирования.
- Код тестов определяется компилятором и не компилируется в конечную сборку.
- Необходимые инструменты можно получить из сторонних библиотек. Особенно полезны testify, suite, gotests.
- Table-driven test — удобный способ организации тестов, который часто встречается на практике.
📂 Go | Последнее изменение: 01.09.2024 14:02