Модульное тестирование
Модульное тестирование, также юнит-тестирование – это проверка корректности отдельных модулей (юнитов) программы. Модулями принято считать блоки кода, содержащие определенный функционал. Чаще всего ими выступают функции и методы.
Модульное тестирование проводится сразу после написания юнита, чтобы проверить, не привело ли изменение кода к появлению ошибок в уже протестированных частях программы. Такой подход позволяет своевременно отлавливать и устранять возможные ошибки.
Познакомимся с юнит-тестами на практическом примере. Создадим примитивную функцию IsEven
, которая принимает на вход число типа int
и возвращает строку “Even”, если оно четное, иначе – строку “Odd”.
Теперь напишем тест для функции IsEven. В Go приняты определенные правила по расположению и наименованию тестирующих файлов и функций:
- Файлы с тестами должны находиться в одном пакете с тестируемыми функциями или в соответствующем пакете с суффиксом
_test
- Название файла с тестами должно оканчиваться на
_test.go
- Название тестирующей функции должно начинаться префиксом Test и далее содержать название тестируемой функции.
Например, тест для нашей функции IsEven
, которая располагается в папке folder
и файле iseven.go
, следует расположить в той же папке folder
, файле iseven_test.go
и назвать TestIsEven
:
Тестирующая функция всегда принимает единственный параметр – *testing.T
, где T
– это тип для управления состоянием теста и поддержки тестовых логов.
Тест заканчивается в тот момент, когда тестирующая функция возвращает или вызывает один из методов: FailNow, Fatal, Fatalf, SkipNow, Skip, Skipf. Стоит учитывать, что они могут вызываться только из горутины, выполняющей тестирующую функцию.
В тестах часто используются методы Log(f)
и Error(f)
. Первый из них форматирует переданный текст и записывает его в специальный лог ошибок. Для обычных тестов этот текст будет выведен при неудачном завершении теста или при указании флага -v
, в то время как для бенчмарков (тестов производительности) он выводится всегда.
Метод Error
под капотом вызывает методы Log
и Fail
, последний из которых при вызове отмечает, что в тестируемой функции есть ошибка, и продолжает выполнение теста.
Запуск тестов
Для запуска тестов достаточно написать в терминале команду go test
. В зависимости от результата тестирования в консоли будет надпись PASS (все тесты пройдены) или FAIL (определенные тесты не пройдены), а также дополнительная информация: путь до go-модуля и время выполнения тестов:
Теперь поменяем местами строки “Even” и “Odd” в функции IsEven и после очередного тестирования получим сообщение об ошибке:
Иногда требуется запустить тесты в определенном пакете. Сделать это можно следующей командой: go test ./<название_пакета>
. Для тестирования всех пакетов используется команда go test ./...
Стоит отметить, что утилита go test
имеет множество флагов, позволяющих детально настроить вывод результатов тестирования.
Например, ранее упомянутый флаг -v
выведет названия всех выполненных тестовых функций, время их выполнения, а также содержимое лога ошибок:
В рамках одной статьи не представляется возможным рассмотреть все флаги, поэтому для детального ознакомления рекомендуем обратиться к соответствующему разделу из официальной документации.
Визуализация покрытия кода тестами
Часто возникает необходимость проверить покрытие кода тестами. В этом поможет команда go test
с флагом -cover
, который выведет процент протестированного кода:
По этой информации трудно понять, какие именно участки кода покрыты тестами, а какие – нет. Более наглядное представление этой информации можно получить с помощью двух последовательных команд:
go test -coverprofile=coverage.out
****- генерация файла с информацией о покрытииgo tool cover -html=coverage.out
– генерация HTML-страницы, наглядно иллюстрирующей покрытие кода тестами
Table-driven тесты
Представим, что необходимо протестировать работу функции IsEven на 5 разных числах. При модульном тестировании придется прописывать много повторяющегося кода с проверкой каждого случая. Такой подход нарушает принцип разработки DRY (Don't repeat yourself), отнимает много времени и подвержен ошибкам.
В подобных случаях рациональнее применить table-driven подход, при котором тест-кейсы организуются в виде таблицы, содержащей заданные входные и ожидаемые выходные значения.
Для создания таблицы используется слайс структур ([]struct
) с несколькими полями, содержащими входные и выходные данные и дополнительную информацию, например, текст.
Далее с помощью цикла for производится итерация по значениям слайса структур с запуском метода t.Run
, который в качестве второго параметра принимает функцию, проверяющую соответствие входных и выходных данных.
Применим разобранную схему table-driven подхода на тесте для функции IsEven:
Запустите приведенный выше тест с флагом -v
и посмотрите на получившийся вывод.
Как вы можете заметить, table-driven тесты упрощают проверку и добавление тест-кейсов и довольно просты в реализации, поэтому являются оптимальным и часто используемым способом тестирования программ.
Параллельный запуск тестов
По умолчанию тесты в Go исполняются последовательно. Чтобы запустить конкретный тест параллельно с другими, следует добавить в него метод t.Parallel()
.
Утилита go test
приостанавливает тесты, содержащие метод t.Parallel()
, и возобновляет их после завершения непараллельных тестов. Стоит отметить, что количество тестов, способных исполнятся параллельно в единицу времени, определяет рассмотренный нами в предыдущей статье параметр GOMAXPROCS
.
Тест можно сделать параллельным, добавив всего три строки, две из которых – это вызов t.Parallel()
в начале теста и внутри метода t.Run()
, а третья – инициализация в цикле for дополнительной переменной для избежания распространенной ошибки использования горутин с итераторами и замыканиями:
Стоит отметить, что упомянутая ошибка была решена после изменения поведения цикла for в версии Go 1.22, поэтому строка tc := tc
имеет смысл только для версий Go < 1.22.
Для простых функций разница в скорости выполнения параллельных и обычных тестов несущественная. Она становится заметна при увеличении количества и сложности тест-кейсов.
Тесты производительности
Тесты производительности (бенчмарк-тесты, бенчмарки) позволяют оценить эффективность кода. Путем многократного запуска одной и той же функции с разными параметрами бенчмарки рассчитывают среднее время её выполнения и объем задействованной памяти.
Стоит учитывать, что результаты бенчмарков напрямую зависят от характеристик и физического состояния компьютера, на котором они выполняются. Архитектура и кэш процессора, скорость RAM, система охлаждения, конфигурация ОС – эти и другие параметры имеют прямое влияние на качество и результативность тестов производительности.
В Go правила для бенчмарк-тестов такие же, как и для обычных, но с некоторыми отличиями: бенчмарки должны начинаться с префикса Benchmark, принимать параметр *testing.B
и содержать тестируемую функцию в цикле for c верхней границей b.N
. Таким образом, тестируемый код будет запущен N раз, причем N автоматически корректируется до тех пор, пока время выполнения каждой итерации не станет статистически стабильным.
Чтобы применить бенчмарки на практике, для начала напишем функцию нахождения простых чисел от 1 до заданного целого числа:
Простейший бенчмарк-тест для функции FindPrimes выглядит следующим образом:
Такой тест дает мало информации об эффективности функции, так как оперирует только одним числом. Более объективным и показательным будет table-driven бенчмарк-тест с несколькими значениями разного порядка, что позволит сделать выводы о работе функции при возрастающих параметрах:
Запуск бенчмарков производится при помощи команды go test
с флагом -bench
и регулярным выражением. Например, для запуска всех бенчмарков используется команда go test -bench=.
, что эквивалентно go test -bench .
Первые 4 параметра (goos, goarch, pkg и cpu) описывают соответственно ОС, архитектуру процессора, go-модуль и маркировку процессора. Далее написаны названия бенчмарк-тестов с указанием входных данных и количества ядер (параметр GOMAXPROCS
), задействованных при бенчмарке, а правее – итерации цикла и среднее время выполнения каждой из них.
Стоит учитывать, что при наличии обычных тестов они также выполняются во время бенчмарков. Для избежания этого поведения следует запустить бенчмарк с флагом -run
и регулярным выражением, не подходящим под названия обычных тестов. Например, -run=^#
или -run=ZZZ
Fuzzing-тестирование
При классическом юнит-тестировании зачастую сложно учесть все возможные варианты вводимых данных. Для разнообразия юнит-тестов случайными, граничными и заведомо неправильными значениями применяют технику Fuzzing-тестирования. Она позволяет покрыть специфические варианты ввода и выявить часто встречающиеся ошибки: выход за пределы массива, деление на 0, утечка памяти, состояние гонки и множество других.
Разработчики языка Go добавили поддержку fuzz-тестов в версии 1.18 и в официальном руководстве сформулировали основные правила их написания. Они схожи с правилами для бенчмарков и юнит-тестов, но содержат дополнительную информацию о допустимых типах и наименовании компонентов.
Задача
В качестве практического задания предлагаем оптимизировать функцию поиска простых чисел с помощью алгоритма “Решето Эратосфена” и написать для нее модульные и бенчмарк-тесты. Некоторые подзадачи потребуют обращения к дополнительным источникам информации, что поспособствует развитию навыка самостоятельного обучения и углублению знаний. Ответы на большинство вопросов содержатся в официальной документации языка Go, поэтому не стоит ею пренебрегать.
Решето Эратосфена
Напишите функцию sieveOfEratosthenes для поиска простых чисел с помощью алгоритма «Решето Эратосфена».
Юнит-тестирование
Напишите юнит-тест по table-driven подходу для функции sieveOfEratosthenes с проверкой пяти и более значений.
Запустите написанный тест одной командой с выполнением следующих условий:
- Выводится процент покрытия кода тестами
- Включено перемешивание порядка выполнения тестов
- Количество запусков теста равно трем
- Количество используемых процессоров равно четырем
Бенчмарки
Напишите бенчмарк-тест по table-driven подходу для функции sieveOfEratosthenes
с входными числами 50, 100, 500, 1000.
Запустите написанный бенчмарк одной командой с выполнением следующих условий:
- Продолжительность каждого теста 3 секунды
- Выводится статистика распределения памяти
В качестве полезного упражнения предлагаем провести бенчмарки с одинаковыми параметрами для функций FindPrimes
и sieveOfEratosthenes
и на основании полученных результатов определить наиболее эффективный алгоритм.
Заключение
В 16 части самоучителя мы затронули важные аспекты тестирования кода, подробно изучили основные виды тестов и на практических примерах рассмотрели их реализацию в языке Go.
В следующей статье изучим основы сетевого программирования и клиент-серверного взаимодействия, рассмотрим инструменты пакета net и напишем первый HTTP-запрос.
Содержание самоучителя
- Особенности и сфера применения Go, установка, настройка
- Ресурсы для изучения Go с нуля
- Организация кода. Пакеты, импорты, модули. Ввод-вывод текста.
- Переменные. Типы данных и их преобразования. Основные операторы
- Условные конструкции if-else и switch-case. Цикл for. Вложенные и бесконечные циклы
- Функции и аргументы. Области видимости. Рекурсия. Defer
- Массивы и слайсы. Append и сopy. Пакет slices
- Строки, руны, байты. Пакет strings. Хеш-таблица (map)
- Структуры и методы. Интерфейсы. Указатели. Основы ООП
- Наследование, абстракция, полиморфизм, инкапсуляция
- Обработка ошибок. Паника. Восстановление. Логирование
- Обобщенное программирование. Дженерики
- Работа с датой и временем. Пакет time
- Интерфейсы ввода-вывода. Буферизация. Работа с файлами. Пакеты io, bufio, os
- Конкурентность. Горутины. Каналы
- Тестирование кода и его виды. Table-driven подход. Параллельные тесты
- Основы сетевого программирования. Стек TCP/IP. Сокеты. Пакет net
Комментарии