как покрывать код тестами
Покрытие кода тестами (Code Coverage) — PHP: Автоматическое тестирование
С ростом проекта, определить какой код протестирован, а какой нет, становится сложно, хотя подобная потребность возникает регулярно. Обычно это происходит тогда, когда в команде есть разные люди и не все из них ответственно подходят к написанию тестов. В таком случае может страдать качество проекта.
Протестированность кода можно измерить. Для этого используют метрику «покрытие кода тестами» (code coverage). Покрытие анализируется тестовыми фреймворками, которые считают отношения строчек, задействованных в тестах, ко всем строчкам исходного кода. Например, если в коде есть условная конструкция, и она не проверяется тестами, это значит, что все строки кода, входящие в неё, не будут покрыты.
Для работы покрытия в PHPUnit необходимо установить расширение xdebug. Проще всего его установить через командную строку:
После выполнения всех тестов, PHPUnit выводит сводную таблицу. В ней показан процент покрытия кода тестами. В примере выше видно что в классе PHP\Package\User покрыто 100% кода, а вот класс PHP\Package\App не тестируется вообще, так как покрытие 0%. При этом общее покрытие кода 83.33%. Обратите внимание, что покрытие сильно зависит от того, какие тесты выполнились. Если часть из них упала с ошибками, то PHPUnit покажет намного меньшее покрытие, так как тесты просто не доберутся до всего кода. Поэтому покрытие меряют только тогда, когда все тесты зелёные.
Эта статистика помогает найти места, где тестов мало. Дальше по ситуации их можно начинать добавлять. Если в проекте тестов не было вообще, то эта статистика начинает быстро расти. А вот дальше, ближе к 90 процентам, придётся бороться за каждую строчку кода.
Однако покрытие само по себе не гарантирует, что покрытый код работает правильно во всех ситуациях. Логические ошибки в коде невозможно отследить только покрытием. Для этого нужны тесты на одну и ту же функциональность, но с разным набором данных. Как правило, это тесты на пограничные случаи. В разработке есть хорошая практика: перед тем как чинить баги, сначала нужно написать тесты, которые их воспроизводят, и только затем уже можно починить их.
Какое покрытие считается допустимым? 100% покрытия выглядит красиво, но добиться его невероятно сложно. И для большинства проектов бессмысленно. Затраченные усилия не окупятся. Большинство разработчиков сходится во мнении, что 80% — это достаточно хорошее покрытие. На этом можно и остановиться.
О метриках тестирования: code coverage для тестировщиков
Как известно из книги «Путеводитель для путешествующих автостопом по галактике», ответ на главный вопрос жизни, вселенной и всего такого — 42. Процент покрытия кода по линиям на одном из моих проектов — 81, дает ли эта цифра ответ на главный вопрос тестирования «cколько тестов достаточно для определения качества продукта»?
В течении своей работы в айти-сфере и тестировании я видела мало команд и проектов, где тестировщики реально используют code coverage в своей работе. Связано это на мой взгляд с двумя вещами:
1. Тем, что тестируем мы прежде всего требования;
2. Далеко не все понимают, как считать и использовать покрытие.
Интересующимся предлагаю свой взгляд на эти 2 пункта.
Требования vs код
Тестировщик тестирует требования. Даже если их формально нет, есть представление о том, как должна вести себя система. Это и только это важно в конечном итоге.
Но.
Не бывает четких исчерпывающих полных требований, проверив каждое из которых, смело можно сказать, что система будет работать как надо и багов нет.
Пример 1
Приложение пытается сохранить данные в БД (располагается на другом сервере). Есть описание того, как оно должно это делать, в том числе звучит требование, что в случае невозможности выполнить операцию (нет доступа к БД, например), мы должы пытаться это сделать до истечения определенного таймаута, потом выдавать клиенту ошибку.
Что значит невозможно выполнить операцию?
Предположим, тестировщик проверяет сценарий с потерей соединения к БД в процессе работы. Все работает хорошо, но значит ли, что багов нет?
В упомянутом приложении мы посмотрели покрытие кода соответствующих классов — оказалось, что разработчик предусмотрел в коде обработку около 5 исключительных ситуаций.
Это значило, как минимум, следующие случаи:
1. Соединение с сервером БД не может быть установлено;
2. Соединение с сервером БД установлено, выполнение запроса вызвало оракловую ошибку;
3. Соединение с сервером БД было установлено, запрос начал выполняться и завис — тут был баг. Приложение ждало ответа примерно минут 5, потом в логи летел эксепшн и больше оно эти данные записать не пыталось.
Пара остальных не стоило внимания по разным причинам.
В примере требования формально проверено было и 1-м кейсом, но баг был найден после анализа покрытия кода. Можно поспорить, что это пример не о пользе code coverage, а о пользе взаимодействия внутри команды (у разработчика детали имплементации можно было бы узнать заранее или дать ему кейсы на ревью), на самом деле я всегда так делаю но не о всем догадаешься спросить, часто внимание к каким-то вещам привлекают непокрытые блоки кода.
Пример 2
В другой системе, которуя я тестировала, при потере консистентности данных приложение должно было выкидывать соответствующий эксепшн, бросать нотификацию мониторингу и ждать, когда придут люди и спасут его. Тесты покрывали разные случаи возникновения таких ситуаций, все обрабатывалось нормально.
Мы посмотрели код, нужный кусок был покрыт хорошо, но я увидела в другом классе непокрытую область кода, в которой бросался тот же самый event о потери консистентности. При каких условиях — неизвестно, т.к. разработчики его быстро выпилили. Оказалось он был скопипасчен из старого проекта, но никто об этом не помнил. Где это могло стрельнуть- неизвестно, но без анализа кода мы бы это не нашли.
Поэтому пусть тестировщик тестирует требования, но если он смотрит еще и код, может поймать то, что в требованиях не описано и хитрые методы тест-дизайна тоже не всегда найдут.
Покрытие = 80. А качество?
Количество не означает качество. Оценка покрытия кода напрямую не связана с качеством продукта, но связана опосредованно.
На одном отчетном совещании я заявила, что покрытие кода у нас увеличилось до 82% по линиям и 51% по условиям, после чего руководством мне был задан вопрос: «А что это значит? Это хорошо или плохо?» Закономерный вопрос, действительно: сколько надо, чтобы было хорошо?
Некоторые разработчики покрывают свой код, добиваясь 100%. Тестировщику 100% добиваться бессмысленно, начиная с какого-то моменты вы столкнетесь с тем, что физически не можете затронуть этот код интеграционными тестами.
Например, разработчики считают хорошим тоном проверять входящие параметры метода на null, хотя в реально работающей системе таких случаев может и не быть (50% по условиям у нас тогда складывалось в том числе из-за этого). Это нормально, передать туда null извне можно было только до первой проверки, которая собственно эту ситуацию и обработает.
К вопросу об «это нормально»: качественная оценка непокрытого кода и ведет в моем понимании к адекватному использованию code coverege. Смотреть важно то, что вы не покрыли, а не сколько. Если это java-код и методы toString(), equals() или ветви с exception, которые сложно воспроизвести интеграционно, ну так и ладно, пусть будет 80% покрытия реальной бизнес-логики. «Лишний» код многие инструменты умеют фильтровать и не считать.
Если сомнения в белых пятнах все-таки остаются, возможно посчитать общее покрытие интеграционными тестами и юнит — разработчики наверняка учли многое что труднодоступно для интеграционных тестов.
Однако есть одно «но». Что, если покрытие кода низкое? 20%, 30%? Где-то я читала забавный факт, что покрытие 50% и меньше (по линиям и условиям, как мне помнится) означает тот уровень тестового покрытия, при котором результат работы приложения будет такой же, как и при отсутствии тестирования вообще. Т.е. там могут быть баги, может не быть багов, с тем же успехом вы могли его и не тестировать. Другое объяснение — много мертвого кода, что маловероятно.
А у нас нет автотестов
А они и не нужны. Даже если вас уверяют в обратном, некоторые разработчики не в курсе, что покрытие можно считать не только для юнит тестов. Есть инструменты, которые пишут покрытие в рантайме, т.е. ставите специально обученный инструментированный билд, проходите на нем тесты, а он пишет покрытие.
А смысл?
Моя знакомая прекрасная тест-лид задала вопрос: «когда тест-кейсы есть не все, и автоматизация в зачаточном состоянии, имеет ли смысл тратить ресурсы на оценку покрытия кода?» Внедрение новых штук в процесс всегда вызывает у менеджмента определенную боль: время, ресурсы и прочие бренности существования, никакого простора для полета тестировщика-мечтателя.
Разберем по порядку, куда конкретно нужно будет потратить ресурсы, если вы решите попробовать считать code coverage:
Пункты 1 и 2 можно отдать разработчикам, могие из них знакомы-слышали-встречались с общеизвестными тулами и тем более смогут построить собственный билд. Построение отчетов, как правило, делается одной командой в командной строке или автоматически, если вы используете CI (у меня это делал jenkins, он же публиковал отчет).
Самое затратное — это четвертый пункт. Основная трудность тут в том, что для адекватной оценки надо уметь читать код, либо садиться рядом с разработчиком, чтобы он объяснял, что значит этот кусок, и как это воспроизвести. Это требует определенной квалификации от тест-инженера и рабочего времени 1 или 2 человек.
Стоит ли оно того — решать команде и ее руководителям. В проектах, где требования слабо формализованы, либо баги возникают необъяснимым для тестеров образом, возможно это может помочь хотя бы понять направление куда копать.
Еще одна категория — проекты, которые предполагают очень hight-level black box тестирование. Это прежде всего тестирование через UI или внешний API систем, внутри которых содержится куча логики, работающей по своим законам, т.е. извне вы не можете ее затронуть или ей управлять, а значит не можете нормально протестировать. Анализ покрытия в таких проектах создаст аргументированную необходимость переходить к более «низким» уровням тестирования: модульным, покомпонентным, тестированию на заглушках и т.п.
Хорошо работает накопленное покрытие кода в цифрах: на графиках можно увидеть моменты, когда вливается новый код, а тесты еще не подоспели; если уровень покрытия был высоким, потом стал снижаться, но предыдущего уровня так и не достиг — где-то может быть хорошее белое пятно недошедших до тестирования требований, и т.д.
Пожалуй, это все, что я хотела сказать на сегодня.
Использование покрытия кода для определения объема протестированного кода
Чтобы определить, какая часть кода проекта в действительности тестируется закодированными тестами, такими как модульные тесты, можно воспользоваться возможностью покрытия кода в Visual Studio. Для обеспечения эффективной защиты от ошибок тесты должны выполнять («покрывать») большую часть кода.
Анализ покрытия кода может применяться и к управляемому (CLI), и к неуправляемому (машинному) коду.
Покрытие кода возможно при выполнении методов тестов с помощью обозревателя тестов. В таблице результатов отображается процент кода, который был выполнен в каждой сборке, классе и методе. Кроме того, редактор исходного кода показывает, какой код был протестирован.
Требования
Функция проверки объема протестированного кода доступна только в выпуске Visual Studio Enterprise.
Анализ покрытия кода
В меню Тестирование выберите Анализ покрытия кода для всех тестов.
Анализ объема протестированного кода также можно запустить из окна средства обозревателя тестов.
Чтобы изменить цвета или использовать полужирный шрифт, последовательно выберите Сервис > Параметры > Среда > Шрифты и цвета > Параметры для: текстовый редактор. В разделе Отображаемые элементы настройте параметры для элементов покрытия, например Области вне покрытия кода.
Если результаты показывают низкое покрытие, проверьте, какие части кода не обрабатываются, и напишите несколько дополнительных тестов для их покрытия. Команды разработчиков обычно стремятся покрыть около 80 % кода. В некоторых случаях допустимо более низкое покрытие. Например, более низкое покрытие допустимо, когда некоторый код создается из стандартного шаблона.
Если полученный результат отличается от ожидаемого, см. статью Устранение неполадок в объеме протестированного кода.
Не забудьте снова выполнить покрытие кода после обновления кода. Результаты покрытия и цвета кода не обновляются автоматически после изменения кода или при выполнении тестов.
Отчеты в блоках или строках
Покрытие кода измеряется в блоках. Блок — это часть кода с одной точкой входа и точкой выхода. Если поток управления программы проходит через блок во время тестового запуска, то этот блок учитывается как покрытый. Количество раз, когда используется блок, не влияет на результат.
Результаты также можно отобразить в виде строк, щелкнув Добавить или удалить столбцы в заголовке таблицы. Некоторые пользователи предпочитают считать в строках, поскольку процентные соотношения более точно соответствуют размеру фрагментов, которые можно увидеть в исходном коде. Длинный блок вычислений учитывается как единый блок, даже если он занимает большое количество строк.
Строка кода может содержать более одного блока кода. В этом случае, если во время тестового запуска все блоки кода были обработаны в любой строке, она учитывается как одна строка. Если обработаны только некоторые блоки в строке, она считается частичной строкой.
Управление результатами по объемам протестированного кода
В окне Результаты покрытия кода, как правило, отображается результат последнего запуска. Результаты изменятся, если изменить данные тестов или если каждый раз выполнять только часть тестов.
Окно покрытия кода также можно использовать для просмотра предыдущих результатов или результатов, полученных на других компьютерах.
Можно объединить результаты нескольких запусков, например тех, что используют различные данные теста.
Чтобы просмотреть предыдущий набор результатов, выберите его в раскрывающемся меню. В меню отображается временный список, который очищается при открытии нового решения.
Чтобы просмотреть результаты предыдущего сеанса, щелкните Импортировать результаты покрытия кода, перейдите к папке TestResults в своем решении и импортируйте файл COVERAGE.
Цвета отображения покрытия могут быть неверными, если исходный код был изменен с момента создания файла COVERAGE.
Чтобы представить результаты в виде текста, щелкните Экспортировать результаты покрытия кода. Будет создан удобочитаемый файл с расширением .coveragexml, который можно обработать в других инструментах или отправить по почте.
Чтобы отправить результаты другому пользователю, отправьте файл COVERAGE или экспортированный файл COVERAGEXML. Получатели могут затем импортировать файл. Если у них та же версия исходного кода, то они смогут увидеть цвета отображения покрытия.
Объединение результатов различных запусков
В некоторых ситуациях будут использоваться разные блоки в коде в зависимости от данных теста. Поэтому может потребоваться объединить результаты различных тестовых запусков.
Например, предположим, что при выполнении теста со входными данными «2» обнаружилось, что покрыто 50 % определенной функции. А при повторном выполнении теста со входными данными «-2» вы видите, что проверены остальные 50 % функции. Теперь можно объединить результаты двух тестовых запусков, чтобы в отчете и представлении расцветки покрытия было видно, что покрыто 100 % данной функции.
Щелкните Экспортировать результаты покрытия кода, чтобы сохранить результаты операции слияния.
Ограничения объединения
При объединении данных покрытия из различных версий кода результаты отображаются отдельно, но они не объединяются. Чтобы получить полностью объединенные результаты, используйте одну ту же сборку кода и изменяйте только данные теста.
При слиянии файла результатов, который был экспортирован, а затем импортирован, результаты можно просматривать только по строкам, но не по блокам. Используйте команду Добавить или удалить столбцы, чтобы отобразить данные по строкам.
При объединении результатов тестов проекта ASP.NET результаты отдельных тестов будут отображаться, но не объединяться. Это относится только к самим артефактам ASP.NET: результаты всех других сборок будут объединяться.
Исключение элементов из результатов покрытия кода
Иногда требуется исключить конкретные элементы в коде из результатов покрытия, например, если код создан из текстового шаблона. Добавьте атрибут System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute в любой из таких элементов кода: класс, структура, метод, свойство, метод задания свойства или метод получения свойства, событие.
Исключение класса не исключает его производные классы.
Исключение элементов в машинном коде C++
Чтобы исключить неуправляемые (машинные) элементы в коде C++, используйте следующий код.
Используйте следующие макросы:
ExclusionName — любое уникальное имя.
FunctionName — полное имя функции. Оно может содержать знаки подстановки. Например, чтобы исключить все функции класса, напишите MyNamespace::MyClass::*
SourceFilePath — локальный путь или путь UNC к CPP-файлу. Оно может содержать знаки подстановки. В следующем примере исключаются все файлы из определенного каталога: \\MyComputer\Source\UnitTests\*.cpp
Поместите вызовы макроса исключения в глобальное пространство имен, а не внутри любого пространства имен или класса.
Исключения можно поместить в файл кода модульного теста или в файл кода приложения.
Чтобы исключить функции в коде C++/CLI, примените атрибут [System::Diagnostics::CodeAnalysis::ExcludeFromCodeCoverage] к функции. Эта процедура не отличается от C#.
Включение или исключение дополнительных элементов
Анализ покрытия кода выполняется только для загруженных сборок, для которых PDB-файл доступен в том же каталоге, что и DLL-файл или EXE-файл. Поэтому в некоторых обстоятельствах можно расширить набор сборок, включенный путем получения копий соответствующих PDB-файлов.
Можно обеспечить более полное управление выбором сборок и элементов для включения в анализ покрытия кода путем записи файла RUNSETTINGS. Например, можно исключить сборки определенных типов, не добавляя атрибуты к их классам. См. дополнительные сведения по настройке анализа объема протестированного кода.
Анализ покрытия кода в Azure Pipelines
После возврата измененного кода ваши тесты будут выполняться на сервере сборки вместе с тестами других членов команды. Анализ объема протестированного кода в Azure Pipelines дает наиболее актуальное и исчерпывающее представление о тестировании в масштабе всего проекта. Такой анализ также подразумевает автоматические системные тесты и другие закодированные тесты, которые обычно не выполняются на компьютерах разработчиков. Дополнительные сведения см. в статье о модульном тестировании сборок.
Анализ объема протестированного кода в командной строке
Для выполнения тестов из командной строки используйте vstest.console.exe. Объем протестированного кода является параметром служебной программы vstest.console.exe.
Запустите командную строку разработчика Visual Studio.
В ОС Windows в меню Пуск выберите Visual Studio 2017 > Developer Command Prompt for VS 2017 (Командная строка разработчика для VS 2017).
В ОС Windows в меню Пуск выберите Visual Studio 2019 > Developer Command Prompt for VS 2019 (Командная строка разработчика для VS 2019).
В командной строке выполните следующую команду:
Диагностика
Если вы не видите результаты проверки объема протестированного кода, см. статью Устранение неполадок в объеме протестированного кода.
Полное покрытие кода
Инструмент тестирования nose
Изначальный пример кода
#!/usr/bin/env python
import operator
Код работает только на Python 2.6 и не совместим с Python 3. Код сохранен в файле main.py.
Юнит-тесты
Начнем с простых тестов:
import unittest
from main import factorial
OK
Добавим еще один класс для стопроцентного покрытия:
class FakeStream :
def readline ( self ):
return ‘5’
Выводы
Адаптация под Python 3
#!/usr/bin/env python
import operator
Теперь программу можно запускать:
$ python3 main.py
Enter the positive number: 0
0! = 1
Значит ли это, что программа рабочая? Нет! Она рабочая только до вызова reduce, что нам и показывают тесты:
$ nosetests3
E. E
======================================================================
ERROR: test_calculation (tests.TestFactorial)
———————————————————————-
Traceback (most recent call last):
File «/home/nuald/workspace/factorial/tests.py», line 9, in test_calculation
self.assertEqual(720, factorial(6))
File «/home/nuald/workspace/factorial/main.py», line 12, in factorial
return reduce(operator.mul, range(1, n + 1))
NameError: global name ‘reduce’ is not defined
FAILED (errors=2)
В данном примере все это можно было обнаружить и ручным тестированием. Однако на больших проектах только юнит-тестирование поможет обнаружить такого рода ошибки. И только полное покрытие кода может гарантировать что практически все несоответствия кода и API были устранены.
Ну и собственно, рабочий код, полностью совместимый между Python 2.6 и Python 3:
#!/usr/bin/env python
import operator
from functools import reduce
import sys
import unittest
from main import factorial
class FakeStream :
def readline ( self ):
return ‘5’
Что такое юнит-тесты и почему они так важны
Бывает, кодишь 10 минут, а дебажишь 2 часа. Чтобы такого не случилось, пилите юнит-тесты. Михаил Фесенко рассказал, как их правильно готовить.
Фесенко Михаил, можно просто Фес. Разработчик, раньше работал системным администратором, пишет на чём скажут, но пока писал на PHP, Go, Python, Bash. Сейчас работает в «Яндекс.Облаке», до этого работал во «ВКонтакте». Любит жену, кино и снимать видео =)
Юнит-тест (unit test), или модульный тест, — это программа, которая проверяет работу небольшой части кода. Разработчики регулярно обновляют сайты и приложения, добавляют фичи, рефакторят код и вносят правки, а затем проверяют, как всё работает.
Тестировать систему целиком после каждого обновления — довольно муторно и неэффективно. Поэтому обновлённые или исправленные части кода прогоняют через юнит-тесты.
Особенности юнит-тестов
На практике используют разные тесты — их разделяют по уровню абстракции с помощью пирамиды Майка Кона :
Чем выше тест в пирамиде, тем больше частей программы он затрагивает. Высокоуровневые тесты «ближе к бизнесу»: они проверяют бизнес-логику и пользовательские процессы. А те, что внизу пирамиды, помогают найти проблемы в отдельных частях кода. Например, какую-нибудь функцию, которая генерирует имя файла.
В отличие от них, юнит-тесты нужны в следующих случаях:
Некоторые программисты пишут только юнит-тесты, а на интеграционные или E2E-тесты жалеют времени. На самом деле нужно покрывать систему всеми видами тестов, чтобы знать, как взаимодействуют друг с другом разные части программы, какие промежуточные результаты они выдают. Но в то же время, если юнит-тесты показывают ошибку, её покажет и интеграционный, и E2E-тест.
Процесс юнит-тестирования
Для юнит-тестирования подключают тестовые фреймворки — они позволяют «мокать», то есть имитировать функции. В коде больших проектов много зависимостей: одна функция вызывает другую и влияет на разные части программы. Но, как правило, достаточно проверить функции «в вакууме», отдельно от остального кода. Для этого и нужен тестовый фреймворк — он моделирует условия, в которых функция А вызывает функцию Б изолированно от других функций.
Простой пример: у нас есть функция на Go, которая получает id бэкапа и возвращает имя бэкап-файла:
Протестируем её с помощью набора входных и выходных данных. Они должны учитывать все ситуации, поэтому не забываем про негативные кейсы — когда программа возвращает ошибку. Вот набор тестовых данных:
В первую очередь я прописал запрещённые данные (-1 и 0) и слишком большое значение (10200300). Когда пользователь их вводит, функция не должна возвращать результат. Вместо этого мы ждём сообщения об ошибке: BAD_ID или BACKUP_ID_TOO_BIG. Когда же функция получает валидный id, она выводит отформатированное имя файла, например Backup#000010.
А вот и код самого теста:
Порой код для тестирования даже больше основного — и это норма. Но иногда всё-таки стоит задуматься, на самом ли деле тест должен быть таким объёмным. Я бы посоветовал покрывать тестами только те фрагменты кода, которые вы планируете менять. Или сложные части, которые, скорее всего, придётся чинить или поддерживать.
Некоторые разработчики мокают всё подряд. Из-за этого тесты становятся хрупкими, а код — сложным и непонятным. На самом деле для юнит-тестирования достаточно лишь немного переписать код, а огромные функции лучше разбить на более мелкие.
В старой хорошей книге «Экстремальное программирование» есть классная мысль: сначала пишите тест, а только потом программу. Это клёвый подход, но не все могут так делать (а кто-то просто не хочет тратить время).
Как покрыть код юнит-тестами
Есть разработчики, которые не проводят модульное тестирование: «Ой, у нас большой проект, и переписать 1000 строк под тесты или замокать их — слишком запарно». На самом деле покрыть код тестами несложно. Вот несколько советов.
Написали код — напишите тест. Я видел много проектов, в которых юнит-тесты писали по принципу «новый код — новый тест». Думаю, это правильный подход, ведь, когда добавляешь в программу что-то новое, она часто ломается. К тому же, если писать тесты сразу, не придётся переворачивать весь код, когда он разрастётся.
Есть более жёсткий принцип: новый код без тестов на ревью не принимается. Конечно, он работает, если сроки не горят, — иначе программист рефакторит или покрывает его тестами позже.
Используйте тестовый фреймворк. В тестировании не нужно изобретать велосипед. Для популярных языков уже есть готовые решения, поэтому достаточно вбить в поиске test frameworks, и вы получите целый список. Вот, например, результат для Python:
Пишите простые тесты. Надо понимать, что происходит с входными данными и какой результат должна вернуть функция. Если непонятно — меняем нейминг и разбиваем функции на более мелкие, избавляемся от зависимостей. Пусть одна функция принимает результат, а другая возвращает. Так проще тестировать.
Допустим, у нас есть такая функция:
Её не нужно прогонять через юнит-тест, потому что тогда придётся мокать process_a, process_b и prepare_output. Тут нужен интеграционный тест, который проверит, как эти компоненты взаимодействуют между собой. Вообще, если код сложно покрывать юнит-тестами, используйте интеграционные — они проверяют общую работу системы, модуля или библиотеки.
Не забывайте про негативные тесты. Это the best practice. Что произойдёт, если передать в программу неправильные данные? Какую ошибку она выведет и выведет ли?
Покрывайте тестами все циклы и if-else. Этот совет касается кода, который нужно поддерживать. Если ему не следовать, на одной из итераций правок вы или ваш коллега просто всё сломаете.
Проверяйте качество тестов. Сделать это поможет мутационное тестирование. Мутационный фреймворк случайно меняет константы и значения в условных операторах и циклах, создаёт копию кода, в которой поочерёдно меняет условия. Например, было >= или было COUNT=3, а стало COUNT=10. Каждая замена тестируется: если код поменялся, а тесты не упали, значит, код не покрыт тестами.
На мутационное тестирование уходит много времени. Можно подключить плагин, который считает code coverage по тесту и выдаёт отчёт. Например, у нас покрыто тестами 43 тысячи строк кода, а 10 тысяч — нет. Значит, code coverage 81%. Но тут важен не только сам процент, но и качество — какие именно фрагменты кода и какими именно тестами покрыты. Например, не всё может быть под юнит-тестами — часть может перекрываться интеграционными.
Обеспечьте достаточный процент покрытия кода. Года три-четыре назад я был фанатиком стопроцентного покрытия. Конечно, безумно круто, когда ты всегда знаешь, что именно сломалось. Но в продакшне этого добиться сложно — да и не нужно. Исключение — маленькие проекты или «жёсткие» команды, для которых полное покрытие в приоритете.
На самом деле, code coverage в 70–90% — уже крутой показатель, но и меньше 70% — тоже плохо. И ещё важный момент: новый код не должен понижать уровень code coverage.
Проверить code coverage можно с помощью coveralls.io:
Coveralls принимает результаты тестов и выдаёт отчёт: показывает процент покрытия и как он изменился с последнего теста.
Не делайте хрупкие тесты. Если тест нестабильный и регулярно падает, его называют хрупким. Его результат может зависеть от дня недели, времени суток, чётности или нечётности запуска. Бывает, две функции работают параллельно и на итоговый результат влияет то, какая из них закончит выполняться первой. Такие функции лучше разбивать на несколько простых и тестировать по отдельности. Мокайте всё что нужно, чтобы сделать тест управляемым, но не переборщите — иначе код будет сложно поддерживать.
Допустим, мы написали юнит-тесты для двух функций. Но не учли, что первая функция сохраняет данные в глобалке, а вторая из-за этого меняет своё поведение. В результате первый тест проходит нормально, а второй падает или ведёт себя странно. А всё потому, что мы не сбросили состояние глобальной переменной.
Следите за скоростью тестов. Тесты должны работать быстро. Если они проверяют кусок кода 10–15 минут — разработчики устанут ждать и отключат их нафиг. Поэтому регулярно проверяйте скорость, ищите узкие места и оптимизируйте тесты. Если есть проблемы, подключитесь через дебаггер — возможно, основной код плохо оптимизирован и искать проблему нужно в продакшне.
Преимущества юнит-тестов
Если у вас ещё остались сомнения, писать юнит-тесты или нет, вот несколько аргументов за. Итак, чем полезны юнит-тесты.
Упрощают работу — находят ошибки, которые вы можете не заметить (меня это много раз спасало). Например, меняешь одну строчку, чтобы поправить логи, а ломается весь код. Благодаря тестам я узнавал об этом ещё до продакшна.
Понятно документируют код. Если вам неочевидно, как работает та или иная функция, можно пройти дальше по коду или открыть юнит-тест. По нему сразу видно, какие параметры принимает функция и что отдаёт после выполнения. Это упрощает жизнь тем, кто работает с чужим кодом.
Помогают ничего не сломать при рефакторинге. Бывает, что код написан непонятно и ты не можешь его отрефакторить, потому что наверняка что-то сломаешь в продакшне. А с тестами код можно смело рефакторить.
Упрощают разработку. Кажется, что юнит-тесты всё усложняют, ведь нужно написать в два раз больше кода — не только функцию, но и тест к ней. Но я много раз убеждался: когда пишешь код без тестов, потом тратишь гораздо больше времени на поиск и исправление ошибок.
Бывает, бац-бац — и в продакшн, а потом понеслось: исправляешь код первый, второй, третий раз. И постоянно вспоминаешь, как тестировать его вручную. У меня даже были файлики с входными данными для таких проверок. Тогда я тестировал программы вручную, по бумажке, и тратил на это уйму времени. А если бы написал юнит-тест, нашёл бы эти баги сразу и не переписывал код по несколько раз.
В коммерческой разработке без юнит-тестов никуда
Сейчас в коммерческой разработке без тестов почти не работают — а в большинстве компаний от разработчиков даже требуют покрывать код юнит-тестами. Везде, где я работал в последние несколько лет, тоже было такое правило. Ведь если в команде кто-то факапит, то может развалиться вся работа — а тестирование как раз защищает от краха.
Современные компании подписывают SLA — гарантируют работоспособность сервиса. Если продукт упадёт, бизнесу придётся заплатить деньги. Поэтому лучше подождать тестов и не катить код, который положит весь продакшн. Даже если сайт или приложение пролежат всего две минуты, это ударит по репутации и дорого обойдётся компании.
Чтобы лучше понять юнит-тесты, изучите тестовые фреймворки вашего языка. А потом найдите крупные open-source-проекты, которые их используют, и посмотрите, как они работают. Можно даже скачать проект и поиграть с тестами, чтобы глубже погрузиться в тему.
Чтобы познать тонкости разработки и тестирования приложений, лучше сразу учиться у практикующих профессионалов. Приходите в университет Skillbox, выбирайте курс и осваивайте программирование под присмотром экспертов.
обложка: Oli Scarff / Staff / GettyImages