В нашей компании в стеке разработки есть язык Go. И иногда, при написании unit-тестов к приложениям написанным на Go, у нас появляются сложности. В этой статье мы расскажем о некоторых моментах, которые мы учитываем при написании тестов. На примерах разберём как их можно использовать.     Об этом говорится в каждой статье про написании тестов, и эта не будет исключением. Интерфейсы уменьшают связанность между пакетами. Но главное их преимущество при написании тестов заключается в том, что интерфейсы позволяют мокать конкретные зависимости. Например, есть у нас функция, которая на основе содержимого Redis принимает какое-то решение о своей работе: Тесты для неё могут выглядеть примерно так:  Но как проверить ситуацию, когда Redis возвращает ошибку? Или что делать, если мы не хотим добавлять Redis в наш CI? То есть как нам замокать вызов Redis? И ответ на эти вопросы — используйте интерфейсы! Перепишем наш код с использованием интерфейсов:  Интерфейсы не только упрощают написание тестов, но и в процессе разработки облегчают переход на другие технологии, например, замену Redis на Memcached. Тест же с использованием мока будет выглядеть примерно так: Понятное дело, что для каждого случая писать свой мок немного избыточно. Можно попробовать написать универсальный мок. А можно попробовать его сгенерировать на основе интерфейса. Существует множество генераторов моков. Нам нравится mockery. Для примера выше написание тестов с использованием генератора могло бы выглядеть следующим образом. Сначала сгенерируем мок для нашего интерфейса: И дальше используем его в тестах следующим образом: Допустим у нас есть код, который логирует свои действия с использованием какой-либо сторонней библиотеки, например, Logrus. И тесты к этому коду:   При запуске тестов мы видим, помимо ошибки, ещё логирование от других тестов:   Если кодовая база большая, то упавшие тесты потеряются среди лишнего логирования. Чтобы такого не было, можно сделать перехват логов в тестах. Для приведённого примера это может выглядеть вот так: И тогда вывод тестов будет лаконичнее, и сразу будет понятно, какое логирование и в каком тесте велось: Здесь приведён пример для Logrus, но нечто похожее можно сделать с любой библиотекой логирования. Например, для библиотеки Zap есть отдельный модуль, который облегчает тестирование. В Go всегда была какая-то странность с подсчётом покрытия кода тестами. Сначала нельзя было построить отчёт по покрытию для всех пакетов, написанных в рамках приложения. До сих пор в некоторых репозиториях можно встретить скрипты, похожие на этот, которые используются для запуска тестов по всем пакетам и объединения информации о покрытии в один отчёт. Сейчас с этим всё хорошо, но есть неочевидный момент.  Допустим, наш проект состоит из трёх пакетов. И мы хотим для них посчитать покрытие. Обращаемся за помощью к утилите cover, которая скажет нам примерно следующее: Пробуем:   Уже из этого вывода видно, что у нас два пакета покрыты на 100 % и для одного пакета нет тестовых файлов. Получим отчёт о покрытии: Но тут что-то не так. В отчёте говорится о полном покрытии тестами. Хотя мы знаем, что это не так. Это всё потому, что при подсчёте покрытия не учитывается пакет, в котором нет тестов. Его не будет и в HTML-отчёте. И кажется, что это не всегда может быть корректным, потому что зачастую мы хотим знать покрытие всего кода, а не только того, для которого мы написали тесты. Это можно исправить, указав специальный параметр при запуске тестов: И теперь отчёт выдаёт ожидаемый процент покрытия тестами: Писать тесты на Go довольно-таки сложно. И если вы разрабатываете какой-нибудь веб-сервис, то иногда бывает проще написать тесты на другом языке, например, на Python, и тестировать приложение как «чёрный ящик».  Но возникает вопрос, а можно ли посчитать покрытие для такого способа тестирования? Да, посчитать можно. В целом, идея довольно проста. Пишем подобный тест:  Запускаем его, потом интеграционные тесты, и завершаем наш тест. Звучит просто, но есть несколько нюансов. Зачастую надо сделать так, чтобы этот тест не запускался со всеми остальными тестами. Он особый, и для него должна быть отдельная логика запуска. Ещё функция main не должна приводить к выходу с ненулевым кодом возврата. И надо реализовать способ выхода из main по сигналу, не завершая при этом сам тест. То есть в целом надо реализовать для нашего web-сервиса graceful shutdown, что несложно сделать, и это в целом полезно. Давайте на примере реализуем небольшой web-сервис, протестируем его с помощью curl, и посчитаем покрытие тестами.  Сервис наш будет выглядеть следующим образом (взято с https://gobyexample.com/http-servers): И тест к нему:   Комментарий +build testrunmain говорит о том, что тест будет запускаться только в случае, если передан соответствующий tag. Запускаем наш тест: Тестируем с помощью curl: И завершаем наше тестирование, нажав Ctrl+C: Можем посмотреть покрытие тестами и увидеть, что обработчик headers остался не протестированным: Мы рассмотрели некоторые вопросы, которые возникали у нас в компании, когда речь заходила о тестировании приложений на Go. Надеемся, что статья будет вам полезна.  Хотите узнать больше о тестировании в Go? Вот ещё несколько интересных статей на хабре: один, два, три.  Открываю закрытые двери