Unit-тестирование
Unit-тестирование — это разновидность тестирования в программной разработке, которое заключается в проверке работоспособности отдельных функциональных модулей, процессов или частей кода приложения. Unit-тестирование позволяет избежать ошибок или быстро исправить их при обновлении или дополнении ПО новыми компонентами, не тратя время на проверку программного обеспечения целиком.

«IT-специалист с нуля» наш лучший курс для старта в IT
Зачем проводится unit-тестирование
Основной смысл модульного тестирования заключается в том, чтобы избежать накапливания ошибок в будущем, а также исключить регрессию уже отлаженных модулей. Например, у вас есть в целом готовое приложение, к которому необходимо добавить несколько новых функций или процессов. Если сначала выполнить интеграцию компонентов, а потом протестировать полностью «собранное» ПО, то ошибки в дополнениях могут привести к нестабильной работе всего приложения. Чтобы этого не произошло, легче протестировать добавляемые функции изолированно, а после устранения всех багов интегрировать их в программу.
Профессия / 8 месяцев
IT-специалист с нуля
Попробуйте 9 профессий за 2 месяца и выберите подходящую вам

Таким образом, unit-тестирование решает следующие задачи:

- поиск и исправление ошибок на ранних стадиях разработки программного продукта и, следовательно, снижение затрат в дальнейшем;
- лучшее понимание разработчиками базового кода проекта, более простая и быстрая корректировка продукта;
- повторное использование кода, в том числе с переносом (вместе с тестами) в другие продукты;
- использование юнит-тестов как проектной документации, по которой разработчики, не знакомые с кодом, могут понять принцип его работы.
Преимущества unit-тестирования
Применять модульное тестирование при разработке программных продуктов рекомендуется по следующим причинам:
- Простота. Написать тест для отдельного модуля проще, чем для приложения в целом. Соответственно, если нужно проверить не всю программу, а лишь ее часть (например, вышедшее обновление или патч), то можно использовать модульное тестирование, предварительно изолировав проверяемый фрагмент кода. Хотя интеграционное тестирование нужно будет провести в любом случае.
- Информативность. Хорошо составленный тест помогает разработчикам понять API приложения, функционал модуля, особенности его использования. Особенно это полезно в том случае, если при работе над проектом произошла смена ответственных за разработку и проверку специалистов.
- Параллельная разработка. Модульное тестирование позволяет проверить работу одного компонента приложения независимо от других. Благодаря этому можно параллельно разрабатывать различные программные модули, тем самым сократив время на создание и отладку продукта.
- Возможность повторного использования. Создав однажды тест для проверки отдельного модуля, разработчик может вернуться к нему позднее, чтобы протестировать работу компонента еще раз. Регрессионное тестирование состоит в написании контрольных примеров для всех функций, которые помогают выявить ошибки, вызванные внесенными изменениями.
Недостатки unit-тестирования
Несмотря на свои достоинства, модульное тестирование не является панацеей от всех болезней кода:
- Модульное тестирование не гарантирует, что будут найдены все ошибки. Причина в том, что даже в относительно простых программах невозможно предугадать все сценарии их выполнения.
- Unit-тестирование применяется к изолированным фрагментам кода, поэтому может выявить только ошибки проверяемого модуля. Оно не способно показать баги, возникающие при интеграции модуля с другими компонентами приложения. Также unit-тестирование не способно выявить системные ошибки продукта в целом.
Модульное и интеграционное тестирование
Часто unit-тестирование путают с интеграционным, но это два разных по реализации и назначению уровня проверки программного обеспечения. Отличительные особенности модульного тестирования:
- узкая специализация — проверке подвергаются отдельные модули, а не все приложение в целом;
- простая реализация — тестирование модулей по отдельности (особенно при параллельной разработке) достаточно легкое в плане реализации, может проводиться без привлечения внешних ресурсов.
Напротив, интеграционное тестирование отличается следующими особенностями:
- общей направленностью — проверке подвергается не каждый модуль, а вся система, включая основное ядро и функциональные компоненты;
- сложностью — интеграционное тестирование проводится в среде, максимально близкой к реальной, поэтому требует привлечения внешних ресурсов (баз данных, веб-серверов).
В реальной практике эти два уровня тестирования не противопоставляются, а дополняют друг друга. Проверка каждого модуля снижает количество багов, которые обязательно проявятся при интеграции компонентов. А интеграционное тестирование позволит оценить взаимодействие программных модулей друг с другом и ядром приложения.

Курс для новичков «IT-специалист
с нуля» – разберемся, какая профессия вам подходит, и поможем вам ее освоить
Виды и методы модульного тестирования
Виды
Ручное. Проводится максимально просто по заранее составленному документу с пошаговыми инструкциями. Однако такой подход возможен только с небольшими и несложными фрагментами кода и к тому же даже в этом случае он занимает много времени.
Автоматизированное. Unit-тестирование заключается в использовании специально разработанных тестовых сред, которые проверяют работу модуля и выявляют в ней ошибки. Такой подход имеет следующие особенности:
- Для каждой функциональной части приложения пишется отдельный модульный тест. Применять один и тот же тест для проверки разных компонентов нельзя.
- Проверяемый модуль должен быть изолирован от ядра приложения и других компонентов, чтобы исключить искажение результатов тестирования. Поэтому модульная проверка проводится не в естественной среде, а в специально разработанной тестовой.
- Использование автоматизированной тестовой среды позволяет смоделировать различные сценарии поведения кода. Если по ходу проверки были выявлены серьезные ошибки, такая система останавливает процесс до их устранения разработчиком, а потом снова запускает тест.
Методы
«Черного ящика». В этом случае тестирование происходит по входным и выходным сигналам модуля без анализа структуры его кода. Чаще всего такой метод применяется, когда проверку выполняет разработчик, который не участвовал в создании компонента.
«Белого ящика». Суть этого метода в том, что тестируются внутренняя структура модуля, его возможности, особенности поведения, реакция на входные сигналы и т.д. Иными словами, компонент изначально полностью прозрачен и понятен разработчику, который оценивает все внутренние и внешние аспекты его работы.
Для понимания unit-тестирования рассмотрим подробнее, как оно происходит по методу «белого ящика». В этом случае оно состоит из трех этапов:
- Анализ отдельного модуля. На этой стадии тестирования разработчик изучает внутреннюю структуру кода, функционал и поведение исследуемого компонента. Данный этап пройдет значительно быстрее, если программист сам создавал модуль или участвовал в его создании. Если нет — ему придется поднимать соответствующую документацию, консультироваться с создателем тестируемого фрагмента кода. Главная задача заключается в полном понимании того, как устроен и работает проверяемый программный компонент.
- Создание кейс-теста. Это сценарий или модель, которые должны показать, как проверяемый модуль ведет себя в реальной обстановке. Кейс-тесты создают искусственную среду, максимально близкую к реальной, но без привлечения внешних ресурсов, которые обычно задействуются в работе программного обеспечения (веб-серверов, баз данных и т.д.).
- Тестирование модуля. Проверяемый компонент, предварительно изолированный от ядра приложения и других модулей, запускается в кейс-тесте. При этом разработчик смотрит на то, как он реагирует на входные сигналы, как работает сам код, соответствует ли его структура выполняемым задачам, анализирует возможные ошибки и т.д.
Часто к одному и тому же компоненту ПО разработчик применяет различные методики тестирования. Указанные методы «черного и белого ящиков» не исчерпывают всех методик и инструментов проверки. Зачастую разработчик создает под каждый проект уникальные способы тестирования, учитывающие особенности программного продукта.
Разработка через тестирование
Стандартна ситуация, когда разработчик сначала написал код, а затем создает под него тест и выполняет проверку. Но в программировании часто используется и обратный процесс: сначала разрабатывается тест, а модуль создается на его основе. Такой подход называется «разработка через тестирование». Суть его в том, чтобы с помощью заранее написанного теста определить требования к будущему программному компоненту. Цикл разработки через тестирование насчитывает несколько этапов:
- Добавление теста. Оно происходит перед добавлением каждой новой функции в программу. Написанный тест не запускается по причине того, что проверяемый фрагмент кода еще не написан. Если тестирование сработало — значит, аналогичная или похожая функция в программе уже есть или тест написан некорректно. Сам тест тоже представляет собой программу, поэтому разработчик предварительно должен четко понять, какие результаты она должна показать в случае успешного тестирования.
- Написание кода. Ориентируясь на то, как должна себя повести тест-программа в «идеальном» случае, разработчик пишет код самого модуля. Причем он не обязан быть сразу совершенным — все неточности будут отшлифованы в последующих циклах разработки. Главное, что требуется от кода, — это прохождение теста. Как только разрабатываемый фрагмент написан, он прогоняется через тест-программу и анализируется.
- Рефакторинг. Убедившись, что написанный модуль успешно проходит тест, разработчик проверяет его на дублирование, неточности, мусорный код и т.д. Задача на этом этапе — максимально очистить фрагмент, сделать его более прозрачным, простым и понятным.
Разработка через тестирование не ограничивается одним циклом: они повторяются каждый раз при добавлении в приложение новых функций, процессов или других объектов. Если в очередной итерации ранее проходивший тестирование код вдруг выдал ошибку, разработчик всегда может откатить внесенные изменения, которые ее вызвали.
Этот метод разработки имеет свои преимущества:
- Код становится более простым и понятным, так как пишется под конкретные требования, заданные в тесте.
- Сокращается время разработки, в том числе за счет более частого использования отката модуля к работающей версии, чем отладки неработающей.
- Дизайн программы становится более удобным для пользователей, так как продумывается заранее, до написания кода, а не подгоняется под него.
- Снижается количество багов, так как разработчик изначально знает, что хочет получить от своего кода, а не использует метод проб и ошибок.
- Заранее написанный тест можно использовать в дальнейшем в качестве проектной документации к программному продукту.
Рекомендации к unit-тестам
Чтобы модульное тестирование было максимально эффективным, тесты должны:
- соответствовать конкретному модулю — нельзя применять один и тот же тест для тестирования разных по назначению и реализации программных компонентов;
- быть автоматизированными — тест лучше вписать в сам код, тогда он будет запускаться автоматически и сильно упростит жизнь разработчику;
- быть своевременными — если тест нельзя написать до разработки самого кода, его лучше создавать параллельно, что сэкономит много времени в дальнейшем;
- отвечать основным задачам — при написании теста не нужно стараться учесть все возможные сценарии, лучше сосредоточиться сначала на основных, а остальные дополнять по мере необходимости;
- иметь хорошее название — описывающее, что именно тестируется, в каких условиях и с каким желаемым результатом.
Когда не стоит проводить unit-тестирование
Модульное тестирование — не универсальный инструмент проверки программного продукта. В некоторых ситуациях оно лишь отнимет время и силы, не показав значимого результата, например:
- при тестировании сложных и разветвленных алгоритмов, таких как красно-черное дерево, придется разработать большое число тестов, что существенно усложнит и замедлит проверку;
- отсутствии четких результатов — например, в математическом моделировании природных процессов, настолько сложных, что их «выход» невозможно спрогнозировать, а можно только описать в виде интервалов вероятных значений;
- тестировании кода, взаимодействующего с системой, — например, модуля, связанного с портами, таймерами и другими «нестабильными» компонентами, от которых его сложно изолировать;
- проверке всего приложения — модульное тестирование не покажет ошибки интеграции, баги ядра и другие аспекты, не относящиеся непосредственно к конкретному модулю;
- недостаточной квалификации самого разработчика и низкой культуре программирования, так как модульное тестирование работает только при строгом соблюдении технологии, постоянном отслеживании всех вносимых в модуль изменениях.
Unit-тестирование окажется бесполезным и при проверке максимально простого кода. Точнее, оно сработает и покажет правильный результат, но сил на написание теста уйдет больше, чем на «ручной» анализ модуля.
Unit-тестирование — это эффективный и полезный инструмент, позволяющий избежать накопления ошибок при разработке программного обеспечения и сильно упрощающий проверку на более высоких уровнях (интеграционную, системную, приемочную).
IT-специалист с нуля
Наш лучший курс для старта в IT. За 2 месяца вы пробуете себя в девяти разных профессиях: мобильной и веб-разработке, тестировании, аналитике и даже Data Science — выберите подходящую и сразу освойте ее.

Статьи по теме:
Кто должен писать юнит-тесты?
Именно ЮНИТ тесты должен писать разработчик, тестировщик просто не сможет грамотно написать тесты для какого-то класса, написать моки и тд.
Функциональные тесты может и тестировщик писать и разработчик.
Ответ написан более трёх лет назад
Комментировать
Нравится 7 Комментировать

console.log(`You’re pulling my leg, right?`);
Есть много разных автоматизированных тестов.
Юнит-тесты пишет разработчик (в идеале — тот же самый, который пишет код, покрытый этими тестами, и ДО написания кода). Юнит-тесты помогают удостовериться, что каждый отдельный кусок (поэтому — unit) в программе по отдельности работает корректно. Имея на руках рабочие юнит-тесты, можно спокойно делать рефакторинг.
Интеграционные тесты помогают удостовериться, что и в совокупности все эти куски выдают осмысленные результаты. Из этих тестов становится понятно, что на сайт можно залогиниться, а софтина пишет в файл в корректном формате. Их тоже часто пишут программисты.
Приемочные тесты помогают удостовериться, что программа соответстует требованиям, заложенным в ТЗ/диздок.
Регрессионные тесты — что после последнего обновления ничего не сломалось. Эти два последних делают тестеры, с помощью различных тулзов — для веба, например, это Selenium.
Все эти тесты, в идеале, запускаются на каждый коммит на специальном Continuos Integration сервере, и если программист «опять накодил в углу», ему приходит письмо, что его код не прошел такой-то тест и это надо починить.
Ответ написан более трёх лет назад
Комментировать
Нравится 2 Комментировать
Просто люблю качественно работать
,Юнит тесты это автоматизированные тесты , причем тут как бы тестировщик , программист написал код и тесты, система сама их гоняет при каждом коммите и сыпет разработчику письмо ты балбес твой юнит тест не проходит, и в этой цепочке нет тестировщика, есть вариант конечно с очень крупными проектами, но мне кажется это не ваш вариант.
Ответ написан более трёх лет назад
Нравится 1 7 комментариев
Алексей Уколов @alexey-m-ukolov
Автоматизированными могут быть не только юнит-тесты. Функциональные автоматические тесты, например, вполне себе тестировщик может писать.
Алексей Уколов: так вопрос про юнит тесты, так то ещё есть автодеплой и континьюс интегрейшен и его вообще по идее должен сисадмин настраивать, но это ведь тоже не будет ответом на вопрос, что юнит тесты должен писать админ.
Алексей Уколов @alexey-m-ukolov
Пума Тайланд: Просто у вас какая-то странная аргументация — юнит-тесты должен писать разработчик, потому что они автоматические.
Алексей Уколов: просто по сравнению от тестировщика он понимает, что он написал и как надо правильно писать эти тесты. Сказать честно ни в одной вменяемой не супер большой команде не видел чтобы отдельно тестировщик писал юнит тесты.
Алексей Уколов @alexey-m-ukolov
Пума Тайланд: я и не говорю, что их тестировщик должен писать — они в зоне ответственности программиста.
Вы написали, что огурцы зеленые, потому что у них пупырышки есть. Я нисколько не спорю с тем, что они зеленые, но пупырышки тут ни при чем.
Что такое модульное тестирование?
Разработка ИТ-продукта — поэтапный процесс; все начинается с идеи, создания концепта, далее этап дизайна, далее написание кода, затем тестирование, и передача готового приложения заказчикам/клиентам/пользователям.
Тестировщик-джуниор в QA-команде понимает, как важен каждый этап, включая тестирование. Этапу тестирования не может быть уделено мало внимания при разработке любого приложения. На этапе тестирования команда, работающая над проектом, проверяет отсутствие багов и соблюдение требований. Разработчики убеждаются, что их продукт соответствует ожиданиям руководства проектом, и будет нравиться пользователям.
Юнит-тестирование — фундаментальный уровень тестовой пирамиды; этот уровень никак нельзя «скипнуть», обойти, или уделить мало внимания; главным образом потому, что юнит-тестирование имеет полезнейшее свойство: дефекты в ИТ-продукте обнаруживают задолго до его выхода на рынок, и вовремя устраняют, пока это можно сделать легко и быстро.
Как правило, юнит-тесты пишут разработчики; в идеале — создатель тестируемого юнита (модуля, компонента). В современной разработке, с непрерывным развертыванием и доставкой, этот процесс более или менее автоматизирован; система не примет юнит с багами (возвращает его разработчику). Ручное юнит-тестирование также распространено.
Юнит-тестирование по объему/количеству тестов составляет, в разных проектах, от 50% до 70% и более. Поэтому современный цикл разработки и тестирования обязательно включает юнит-тестирование; и тестировщик-джун должен обладать базовыми понятиями о юнит-тестах; в крупных компаниях в некоторых проектах случается, что и тестировщики пишут юнит-тесты чужого кода.
Что такое юнит-тестирование: определение
Юнит-, или модульное тестирование — «первый снизу», фундаментальный уровень (функционального) тестирования. Цель модульного тестирования состоит в проверке каждого отдельного юнита (модуля) продукта на ранней стадии разработки, чтобы не допустить проникновения ошибок («каскадом») на верхние уровни.
Чтобы правильно ответить на вопрос, что такое юнит-тест, нужно сначала правильно сформулировать, что такое юнит, с точки зрения тестировщика?
Юнит — это самая мелкая и самая простая часть продукта, которая должна быть протестирована. Юнитом может быть один метод, процедура, объект, или модуль кода. Обычно юнит-тест требует лишь одного или нескольких вводов (входных значений), и генерирует на выходе (один) определенный результат (который, собственно, и проверяется при unit-тестировании).
Почему юнит-тестирование незаменимо
У джуна QA все же может возникнуть вопрос, зачем же нужно тестирование на таком низком уровне, и почему столько внимания. Почему бы не придумать какие-то сверхновые, суперсовременные, полностью автоматические методики разработки, вообще НЕ предполагающие тестирование на таком уровне? Как-то упрощающие процесс. Передающие эту механическую, объемную работу на уровне модулей — куда-то повыше, к Вершине Пирамиды, тем самым передавая и ответственность за тестирование продукта полностью в QA-департамент; освободить «разрабов» от рутинных задач; а мы, тестировщики, разберемся со всеми тестами, начиная с самых простых. Ответ в том, что если это было легко сделать, то так делали бы все и давно. Но с этим сложности.
Во первых. Юнит-тестирование, как подход, пока что незаменимо, в той форме в какой оно существует сейчас, поскольку оно на фундаментальном уровне помогает ИТ-компаниям и отдельным разработчикам улучшать качество своих продуктов/модулей. Разработчик, который сам пишет юнит-тесты к своему коду, развивается как разработчик; и ему писать юнит-тест проще, удобнее, и быстрее, чем кому-либо другому.
Вторая причина: юнит-тестирование создает проверенный, надежный, реюзабельный код модулей — и значит эти модули могут быть где-то применены в каких-то других продуктах или компонентах этого же приложения. Что тоже имеет значение, особенно в больших проектах — реюзабельность.
Третья причина: разработка ускоряется, и идет более гладко, упорядоченно. Разработчикам, которые сами пишут юнит-тесты своих модулей, потом легче дается рефакторинг и вообще совершенствование продукта.
Четвертая причина: юнит-тесты улучшают ситуацию с документацией продукта; документация становится более простой, понятной, как для разработчиков, так и для тестировщиков.
Пятая причина: если продукт на низком, unit-уровне хорошо покрыт тестами, то как правило эти модули лучше интегрируются с другими модулями и частями продукта, и внешними инструментами и технологиями.
Юнит-тестирование нужно потому, что:
- Повышение скиллов у разработчиков
- Фреймворки юнит-тестирования существуют уже десятилетиями, хорошо знакомы разработчикам, как правило надежные и хорошо документированные
- Легче освоить техники статического тестирования (walkthrough/review/inspection), что будет полезно для повышения уровня в ЯП
- Добросовестно выполненное юнит-тестирование экономит компании время и деньги, позволяя найти ошибки на очень ранней стадии разработки. Не тратить время и деньги, неделями пытаясь найти и устранить не всегда воспроизводящиеся баги, а релиз вот-вот.
- Такое тестирование позволяет команде принять и внедрить у себя методику разработки через тестирование (TDD), в которой тест-кейсы создаются до написания кода. (Традиционный подход: сначала пишем код, потом тестируем).
Как это работает
Юнит-тест — это программный код, который проверяет, что модуль работает корректно. Под «корректно» подразумевается, что модуль возвращает нужный результат (выполняет нужную функциональность, выводит ожидаемые данные).
Если так не происходит, если тест не выдает ожидаемый результат, он считается непрошедшим, то есть failed («красные тесты»). Если тест выдал ожидаемый результат, то есть прошел (passed, «зеленый»), можно переходить к следующим тестам/этапам.
Кто должен писать юнит-тесты
Отдельно уточним. Как говорилось выше, юнит-тесты — изначально и всегда была сфера ответственности разработчиков. Но их пишут и тестировщики; quality analysts на Западе могут, и, как считают некоторые PM-ы в не самых крупных ИТ-компаниях, даже должны писать юнит-тесты, если их об этом просят.
Возможно, такой подход оправдан, по крайней мере с точки зрения проджект-менеджера, с точки зрения стратегии обеспечения качества созданной им, написание юнит-тестов может быть обязанностью тестировщиков как часть их общей задачи в QA. Однако, сложность обычно в том, что юнит-тестирование выполняется на ранних этапах разработки, когда кодовой базы еще не существует, знания и тем более понимания кода у тестировщиков разумеется нет, и тестировщики просто не смогут написать качественные юнит-тесты. Разработчики до сих пор пишут юнит-тесты и так будет всегда, потому что они знают свой код лучше, и у них юнит-тесты получаются быстрее и надежнее.
Также причины, почему разработчики пишут юнит-тесты:
- Разработчикам лучше известно, какие части кода критически важные и их нужно проверить тщательнее
- Они намного более компетентны, когда дело касается моков/заглушек
- Они делают это быстрее, имея намного больше опыта в программировании, чем любой QA-автоматизатор
- И это важнее всего — экономия времени, что критически важно в современных проектах «по эджайлу».
В некоторых ситуациях тестировщиков привлекают к написанию юнит-тестов, особенно что касается не очень важных модулей, высвобождая разработчиков для рефакторинга или продолжения работы с продакшен-кодом.
О важности юнит-тестирования в Agile
При создании продукта по Agile особенно важен поиск и устранение потенциальных дефектов на ранних стадиях разработки. Юнит-тестирование — проверка соответствия продукта требованиям на фундаментальном уровне, поэтому в каждом билде «по эджайлу» должно быть проведено юнит-тестирование, и сгенерирован репорт, описывающий функциональность (и проблемы с ней) на уровне юнитов; и QA-команду автоматически информируют о неудачных тест-кейсах.
Какие юнит-тесты можно считать добротными?
- Быстрые. В больших корпоративных проектах, возможно, сотни тысяч отдельных модулей. Поэтому хороший юнит-тест выполняется быстро (миллисекунды). Часто тесты нужно повторять многократно. Поэтому юнит-тест по определению должен быть очень быстрым.
- Легкий дебаг. Всякий юнит-тест должен быть достаточно простым и ясным, как бы «объяснять поведение модуля». Некоторые тесты выполняются успешно будучи изолированными, но падают после интеграции модулей, что указывает на ошибки в дизайне.
- Независимые и изолированные. Хорошие тест не зависит от внешних факторов, например типа файловой системы или базы данных.
- Самопроверка. Автоматизированные юнит-тесты в идеале должны «предвидеть», пройдут они или нет, без вмешательства человека.
- Воспроизводимость и стабильность. Хороший тест должен выдавать стабильные результаты всякий раз.
Подходы юнит-тестирования
Тестирование белого ящика
Также известно как «тестирование стеклянного ящика» или «прозрачное». Разработчик (или тестировщик) знает код приложения и понимает его функциональность. Поэтому может верифицировать модуль лучше — понимая его код и связи с другими модулями.
Черного ящика
Противоположный подход: разработчик не знает кода, может судить о модуле только по его поведению, поэтому эта техника еще называется «поведенческим тестированием». Тестировщик/разработчик не знает внутреннего строения модуля и его связей, и тестирует только функциональность.
Серый ящик
Так называемое «полупрозрачное тестирование», смешение описанных выше подходов. Разработчик имеет ограниченное понимание кода модуля. Применяется тестирование по паттернам, матричное тестирование, ортогональных паттернов, и регрессионное.
Процесс
Разработчик создает код проверки функции модуля. Он также может изолировать эту функцию, чтобы проверить ее более тщательно. Когда функции изолированы, могут проявиться какие-то нежелательные зависимости между модулями, что позволяет устранить их.
Как уже сказано, разработчики и тестировщики могут выполнять юнит-тесты вручную или автоматизировать процесс, что предпочтительно, исходя из большого объема рутинных задач. Ручное юнит-тестирование утомительно и требует много времени, зато позволяет проверить модули более качественно, пошагово.
В Python применяется фреймворк UnitTest или другие подобные для создания автоматизированных тест-кейсов.
- Разработчик пишет код проверки функции модуля. Далее, когда продакшн-код деплоится, он может закомментировать или вообще удалить код теста.
- Если нужно, разработчик может изолировать функцию, чтобы лучше ее проверить, особенно проблемные зависимости.
- Фреймворк записывает все неудачные результаты автотестов. В некоторых фреймворках есть удобные обобщенные репорты.
Недостатки юнит-тестирования
- Сложность и стоимость, при имплементировании новых функций в приложение
- Замедляет создание прототипов когда исходный код приложения часто обновляется
- Тесты с взаимными зависимостями могут влиять на результаты
Сложности
- Названия и метки. Присвоение тестам понятных названий и меток важно для команды.
- Понимание всего кода. Юнит-тестов всегда очень много, соответственно объем кода. Чтобы писать хорошие тесты, желательно изучить весь код.
- Низкий уровень опыта что касается моков. Случается, что моки сложнее, чем продакшен-код.
- Дебаг требует много времени. Если тесты часто падают, юнит-тестирование задерживает разработку.
Лучшие практики
- Как уже говорилось выше, юнит-тесты должны быть автономны. Любые зависимости будут влиять негативно, усложняя дебаг тест-кейсов.
- Тестировать много use-кейсов. Один юнит может быть связан с несколькими use-кейсами, поэтому нужно тестировать каждый use-кейс в разных тест-кейсах, что позволяет рефакторить код более эффективно.
- Паттерн ААА, отделяющий объект тестирования от arrange-этапа, что делает тест-кейсы более читабельными.
- Логичные и понятные названия переменных, тест-кейсов, сценариев.
Пример юнит-теста
Юнит-тест проверяет часть кода, класс, или просто один метод. Чем меньше тест, тем лучше, небольшие тесты скорее выполняются, и их легче запускать «пакетом».
Далее пример компактного юнита кода и юнит-теста. Это класс, суммирующий две цифры, и простая тестовая функция этого класса.
public class Adder < public int addnum(int num1, int num2)< return num1 + num2; >> import org.junit.Assert; import org.junit.Test; public class Addertest < @Test public void test addnum()< Adder adder1 = new Adder(); int num1 = 3; int num2 = 2; int result = adder1.addnum(num1, num2); Assert.assertEquals(5, result); >>
Инструменты
JTest
Плагин для IDE с открытым кодом, в один клик создающий, масштабирующий и обслуживающий юнит-тесты; помогает автоматизировать процесс и экономит время, высвобождая время команду для бизнес-логики.
JUnit
Бесплатный инструмент для Java. Поддерживает assertions.
NUnit
Бесплатный фреймворк для .NET с открытым кодом. Поддерживает DDT-методику и параллельное выполнение.
JMockit
Инструмент покрытия кода на Java. Есть функции записи и проверки синтаксиса для моков API. Подсчитывает покрытие по строкам, путям, и данным.
EMMA
Открытый инструмент анализа и репортов Java-кода, аналогичный JMockit. Поддерживает больше типов покрытия кода.
PHPUnit
Для разработчиков на PHP. Есть встроенные assertions.
Бонус: что такое TDD
Разработка через тестирование (TDD), иначе называемое TFD, подход «сначала тесты, потом код к этим тестам». Итеративный метод разработки, когда разработчики пишут тест-кейсы до написания продакшен-кода. Тест-кейсы создаются по каждой функции до появления ее кода. Ставится цель изменить сам процесс разработки, и как утверждают адепты этой методики, она позволяет минимизировать количество багов в финальном продукте.
Тесты в TDD содержат условия, которым продакшн-код должен соответствовать. Каждый тест-кейс описывает и затем валидирует то, что должен делать продакшен-код. Затем пишется продакшен-код и проверяется этими тест-кейсами; если код падает, разработчики рефакторят его, пока не достигнут результата, то есть соблюдения требований в тест-кейсе.
- Создание тест-кейсов, проверяющих какую-то функциональность.
- Коррекция кода, если тесты падают. Делаются корректировки, нужные чтобы тест прошел.
- Рефакторинг кода. Если код прошел все тесты, разработчики оптимизируют свой код, выбрасывают все избыточное и конфликтующее.
Освоить разработку через тестирование достаточно сложно и под силу только опытным разработчикам. Но если TDD уже освоен командой, это будет ценный инструмент в арсенале. Продукты получаются качественные, как утверждают.
Более серьезный материал о модульном тестировании — Юнит-тесты. Очень глубокое погружение
Home

Привет! Меня зовут Владимир, я разработчик команды продукта «Сервис персонализации» в SM Lab. В этом посте я хотел бы рассказать (а в комментариях — обсудить) один очень важный и полезный инструмент разработчика — юнит-тесты.
Вы наверняка уже много про них знаете, особенно если они составляют часть ваших рабочих обязанностей. Информации в сети много, проблема в том, что она не всегда полная и недостаточно хорошо структурирована.
Мой рассказ будет состоять из двух частей. В этой части я расскажу, что такое юнит-тестирование и для чего это нужно, что такое покрытие тестами, как оно считается и какие есть подводные камни, рассмотрю подходы к изоляции в юнит-тестах и виды зависимостей, а также вопросы, связанные с эффективностью юнит-тестов.
Эта статья для всех – кто слышал про них, но не видел, кто приступает к написанию юнит-тестов, и кто их пишет уже давно. Надеюсь, каждый из вас найдет что-то полезное для себя.
При подготовке материала очень помогла книга Владимира Хорикова (@vkhorikov ) «Принципы юнит-тестирования». Рекомендую ее всем, кто хочет еще глубже погрузиться в эту тему.
Что такое юнит-тестирование и для чего оно нужно
Приведу одно из устоявшихся определений: юнит-тестирование – это процесс, который позволяет проверить работоспособность отдельных частей исходного кода программы.
Зачем мы пишем юнит-тесты?
Во-первых, это способ проверить работу нового функционала, который мы пишем. Также в процессе развития программного продукта код периодически нужно рефакторить. В этом случае юнит-тесты позволяют проводить рефакторинг без опасений, что существующая функциональность будет сломана. Помимо этого, юнит-тесты позволяют облегчить обнаружение ошибок и локализовать их поиск. Другое полезное свойство юнит-тестов в том, что, читая их, можно понять, как работают части системы и как их нужно использовать. Это документация, которая живет и меняется вместе с кодом.
Какими качествами должен обладать юнит-тест
Таких качеств всего три, и они достаточно общие. Юнит-тест должен проверять правильность работы небольшого фрагмента кода – юнита, должен делать это быстро и поддерживать изоляцию от другого кода.
По поводу «делать это быстро». В области юнит-тестирования достаточно тяжело определить какие-то границы. Если взять тест, который выполняется за секунду — быстро это или медленно? Всё зависит от того, что это за тест, какие требования к нему. В нашей команде мы опираемся на субъективное восприятие скорости – если, по нашему мнению, тесты проходят быстро – этого достаточно.
Как тесты влияют на разрабатываемые нами продукты
Давайте взглянем на следующий график:

Горизонтальная ось «Прогресс» — своего рода жизненный путь разрабатываемого продукта во времени, где нулевая точка — начало разработки этой системы. В начале жизни продукта развивать его достаточно просто: еще не приняты неудачные архитектурные решения, нет кода, который нужно поддерживать или рефакторить. Поэтому в начале мы можем увидеть, что разработка без тестов, если мы будем сравнивать с другими кривыми, требует минимального времени.
При дальнейшем развитии, когда продукт обрастает кодовой базой и становится сложнее, скорость разработки начинает замедляться, мы тратим больше времени на одинаковый объём доработок. И по графикам видно, что с хорошими тестами увеличение времени достаточно линейно.
С плохими тестами или без тестов рост времени нелинеен, и иногда даже наступает такой момент, когда проще отказаться от текущей реализации системы и переписать ее заново, чем пытаться в неё что-то внести. Исходя из этого, стоит оценивать покрытие кода тестами с точки зрения его жизненного цикла. Если продукт одноразовый, маленький, короткий, который мы написали и потом через какое-то время он будет не нужен, то и, возможно, уровень покрытия тестами нужен достаточно низкий.
Тесты имеют хорошее свойство выступать индикаторами низкого качества кода, которое обычно проявляется в высокой связанности, когда части кода плохо изолированы друг от друга, и это создаёт сложность с раздельным тестированием этих частей. Поэтому сложность покрытия юнит-тестами – это хороший негативный признак. Если тяжело писать тесты, то, скорее всего, покрываемый тестами код низкого качества. Правда, это работает только в одну сторону. Мы не можем сказать о том, что если легко писать тесты на код, то он хорошего качества.
Также хочу предостеречь от попыток тотального покрытия кода тестами. Стоит помнить о том, что тесты так же, как и основной код, имеют свою стоимость. Это можно увидеть на предыдущем графике – разность по вертикальной оси между началами графиков – без тестов и с тестами. Эта дельта времени и есть стоимость тестов на старте. Тесты требуют как разработки, так и поддержки. Плюс к этому, чем больше кода написано, тем больше потенциальных ошибок там может быть. На тесты мы не пишем тесты, поэтому большой объём тестового кода может содержать ошибки. Также увеличивается время прохождения тестов и уменьшается желание их часто запускать.
Как измерить покрытие кода тестами
Для этого есть две чаще всего используемые метрики – процент покрытия строк (code coverage) и процент покрытия логических ветвей (branch coverage) кода.

Как и с легкостью покрытия кода тестами, это хороший негативный признак, но плохой позитивный. Если покрыто всего десять процентов кода, это хороший признак того, что тестов слишком мало.
Но даже стопроцентное покрытие тестами не гарантирует хорошего качества тестов. И вот почему. Эти метрики не отражают некоторые важные детали, такие как наличие и полноту проверок (asserts). Мы можем написать тест, который не имеет проверок вообще. То есть он будет запускать наш основной код, но по факту не будет иметь никакой ценности. И ещё, эти метрики не оценивают код во внешних библиотеках. Они оценивает только тот код, который написали мы.
Вот короткий пример:

Есть метод, который принимает строку и превращает её в число. Очень простой метод. На него написан простой тест, который передаёт строку со значением «5» и сравнивает число 5 с тем, какое число возвращает метод.
Если мы посмотрим на этот тест и попробуем оценить метрики покрытия, мы увидим, что для этих метрик покрытие будет 100%. Но, к сожалению, если мы заглянем в библиотечный метод parseInt, мы увидим, что на самом деле мы проверили только один из четырех вариантов. Поэтому с одной стороны у нас покрытие 100%, но с другой стороны мы покрыли по факту только 25% кода. Поэтому на эти метрики стоит ориентироваться, но не слепо им доверять. В нашей команде мы ориентируемся на уровень в 80% покрытия кода.
Тестовые двойники
Помощниками в тестировании выступают «тестовые двойники» (test doubles). Их выделяют 5 видов.

Пустышка (Dummy). Такой двойник не содержит поведения и используется в качестве заполнителя параметров. Никогда реально не вызывается.
Подделка (Fake). Это реально написанная реализация, которая имеет более простую логику, чем реальный аналог.
Есть ещё три вида похожих между собой тестовых двойников. Но есть и различия.
Заглушка (Stub) — имеет заранее подготовленные ответы на вызовы методов. Практически не имеет логики.
Шпион (Spy). Более сложная система. Это гибрид реального объекта и мока. Он имеет поведение реального объекта, но может записывать определенную информацию о вызове его методов. Также можно переопределить поведение некоторых методов.
Мок (Mock). Самый сложный из тестовых двойников, но дающий наибольший контроль. Может иметь сложную логику ответов, зависящих от параметров вызовов, их количества, генерировать исключения при вызове методов с неопределенным поведением и имеет другой, полезный для тестирования функционал.
Все тестовые двойники, за исключением fake, в основном создаются с помощью фреймворков, с которыми мы часто работаем. Для Java это Mockito, для Kotlin – MockK, для других языков такие фреймворки тоже есть.
Изоляция и виды зависимостей
Что такое зависимость? Класс чаще всего не существует изолированно в коде. Он использует какие-то другие части программы. Зависимости – это то, что использует класс для своей работы. Это могут быть другие классы, базы данных, файловые системы, сторонние сервисы и прочее.
Зависимости с точки зрения изменяемости делятся на изменяемые – те, состояние которых может изменить тест, например, переменные или база данных, и неизменяемые – например, константа или неизменяемый объект. Зависимости с точки зрения возможности влияния тестов друг на друга делят на совместные и приватные.
Совместные зависимости (shared). Через такие зависимости тесты могут влиять на результаты друг друга. Например, статические изменяемые поля класса, база данных. Это изменяемые зависимости.
Приведу пример — два теста работают с одной и той же таблицей в базе данных. Если такие тесты будут запущены параллельно, то может получиться ситуация, что один тест добавит данные в таблицу, а другой сразу после этого может очистить таблицу. Первый тест попытается прочитать данные из таблицы, и, не обнаружив нужных данных, завершится ошибкой.
Приватные зависимости (private). Это зависимости, не являющиеся совместными. Они могут быть как изменяемыми, так и неизменяемыми. Та же база данных может быть как совместной, так и приватной зависимостью. Совместной она может выступать, когда все тесты работают с одной базой. Приватной зависимостью база данных может выступать в случае, когда для каждого теста поднимается отдельная база данных, например в docker-контейнере. В этом случае тесты не смогут повлиять друг на друга через базу.
Есть два подхода (школы) к пониманию изоляции: так называемые классическая и лондонская школы.
Лондонская школа понимает изоляцию тестируемого кода как изоляцию от его изменяемых зависимостей. Все изменяемые зависимости (совместные и приватные) заменяются на тестовые двойники – «мокируются».

В этом подходе есть следующие плюсы:
- можно разбить взаимосвязанный граф классов на отдельные классы, заменив изменяемые зависимости тестовыми двойниками, и тестировать их независимо.
- позволяет однозначно определить, что юнит – это класс.
- в случае падения теста поиск проблемы ограничивается одним классом.
Есть и минусы. Основной — то, что в этом случае тесты чаще привязываются к деталям имплементации тестируемой системы, что вызывает хрупкость тестов. Также при большом количестве зависимостей настройка моков может быть достаточно трудоемкой.
Мы в команде чаще всего используем этот подход.
Классическая школа подразумевается изоляция не тестируемого кода, а тестов друг от друга. Если тесты через зависимость могут влиять друг на друга при параллельном запуске, то эта зависимость меняется на тестовый двойник. Все остальные зависимости, даже если они изменяемые, используются как есть. То есть при таком подходе зачастую тестируется одновременно несколько связанных классов.

У этого подхода также есть плюсы и минусы. Для тестирования необходимо выстраивать граф классов, это становится достаточно сложным, когда используется много зависимостей. С другой стороны, не скрываются проблемы плохого дизайна кода и его высокой связанности.
При таком подходе юнитом является не класс, как в Лондонской школе, а единица поведения, и она часто не ограничивается одним классом. При изменении одного из классов падают тесты и всех зависимых классов. Иногда тяжело понять причину массового падения тестов. Для упрощения отладки стоит чаще запускать юнит-тесты, что поможет понять, какие изменения в коде повлияли на тесты и локализуют поиск. Это также позволяет понять, какие классы наиболее важные и на какие нужно обратить особое внимание.
Эффективность юнит-тестов
Чтобы формально как-то измерить эффективность наших тестов, мы можем представить ее как произведение четырёх параметров.
- защита от багов – возможность выявить ошибку, которая зависит от объема выполняемого кода, его сложности и важности.
- устойчивость к рефакторингу – возможность пережить рефакторинг тестируемого кода без выдачи ошибок, минимальные ложные срабатывания.
- быстрая обратная связь – быстро выполняемые тесты ускоряют обратную связь.
- простота поддержки – насколько сложно понять и запустить тест.
Пусть каждый из параметров будет числом от 0 до 1. В соответствии с этим можно понять, что если какой-то из параметров равен нулю, то и, соответственно, вся эффективность, насколько бы высокими ни были другие параметры, будет также равна нулю.
Давайте рассмотрим следующие отношения между поведением тестируемого кода и теста:

В случаях, когда функциональность работает правильно и тест проходит (отрицательное срабатывание) или функциональность работает неправильно и тест не проходит (истинное срабатывание) – это ожидаемое поведение теста. В этом случае можно считать, что тест работает правильно.
Есть ещё и две другие области. Ложное срабатывание — функциональность работает правильно, но тест не проходит. Это значит, что тесты, имеют низкую устойчивость к рефакторингу. Чаще всего это означает, что тесты завязаны на детали имплементации и будут падать при изменении реализации тестируемого кода без изменения функциональности. Низкая защита от багов – случай, когда тест проходит, но функциональность работает неправильно.
Из четырех атрибутов для юнит-тестов один должен быть высоким – это простота поддержки, потому что иначе на тесты будет потрачено недопустимо много времени из-за их большого количества. Простота поддержки – характеристика независимая. Оставшиеся три – взаимосвязаны.

Сквозные тесты (e2e-тесты) задействуют большой объём кода, проходя через цепочку тестируемых элементов или систем, поэтому имеют хорошую защиту от багов. Такие тесты устойчивы к рефакторингу, так как ориентируются на внешний интерфейс – API, и ничего не знают про детали реализации. Такие тесты достаточно медленные — и с точки зрения написания, и с точки зрения прохождения.
Тривиальные тесты могут быть устойчивы к рефакторингу и иметь быструю обратную связь. Например тест, написанный на getter – метод получения значения приватного поля. Такой тест переживет рефакторинг getter-а (если такой вообще будет) и будет быстро проходить. Но пользы от такого теста не будет.
Можно написать тест, который будет хорошо защищать от багов, иметь быструю обратную связь, но при любом малейшем рефакторинге тестируемой системы будет падать. Он будет падать даже тогда, когда тестируемая функциональность работает правильно. Сначала на такие тесты перестают обращать внимание, потом их отключают или удаляют. Пользы от таких тестов тоже мало. Это хрупкие тесты.
Перед тем как разобраться, на какие из характеристик стоит ориентироваться при написании различных тестов, напомню про всем известную пирамиду тестирования

Участки пирамиды подразумевают количество необходимых тестов данной категории. Чем больше площадь, тем больше необходимо тестов. Чем ниже находятся на пирамиде тесты, тем:
- проще и быстрее они должны разрабатываться.
- ниже затраты на поддержку тестов.
- быстрее скорость прохождения отдельного теста.
Перечисленные факторы влияют на выбор между устойчивостью к багам и быстрой обратной связью.
Устойчивость к рефакторингу желательно держать максимальной, поэтому что это величина достаточно бинарная – тесты либо устойчивы к рефакторингу, либо нет. Защита от багов и быстрая обратная связь – более эластичны, поэтому выбор идет между ними.

Сквозных тестов меньше всего – они довольно медленные и не всегда отличаются простотой в поддержке. Но они задействуют большой объем кода и покрывают в основном критичную функциональность, поэтому у них высокая защита от багов.
Юнит-тестов в основном самое большое количество, поэтому для них важна скорость выполнения.
Количество интеграционных тестов меньше, чем юнит-тестов, но больше, чем сквозных, поэтому они занимают промежуточное положение между защитой от багов и быстрой обратной связью.
На этом первая часть закончена. В следующей части будет рассмотрена структура юнит-теста, поделюсь тем, какие подходы используются у нас в команде. Расскажу про стили юнит-тестирования, принципы рефакторинга для эффективных юнит-тестов, рассмотрю некоторые антипаттерны при написании тестов.