Какие преимущества дает применение практики модульного тестирования
Unit testing (юнит тестирование или модульное тестирование) — заключается в изолированной проверке каждого отдельного элемента путем запуска тестов в искусственной среде. Для этого необходимо использовать драйверы и заглушки. Поэлементное тестирование — первейшая возможность реализовать исходный код. Оценивая каждый элемент изолированно и подтверждая корректность его работы, точно установить проблему значительно проще чем, если бы элемент был частью системы.
Unit (Элемент) — наименьший компонент, который можно скомпилировать.
Драйверы — модули тестов, которые запускают тестируемый элемент.
Заглушки — заменяют недостающие компоненты, которые вызываются элементом и выполняют следующие действия:
- возвращаются к элементу, не выполняя никаких других действий;
- отображают трассировочное сообщение и иногда предлагают тестеру продолжить тестирование;
- возвращают постоянное значение или предлагают тестеру самому ввести возвращаемое значение;
- осуществляют упрощенную реализацию недостающей компоненты;
- Имитируют исключительные или аварийные условия.
- таким способом невозможно найти взаимоуничтожающихся ошибок,
- некоторые ошибки возникают достаточно редко (ошибки работы с памятью) и потому их трудно найти и воспроизвести
Стратегия модульного тестирования
Модульное тестирование является одной из ключевых практик методологии экстремального программирования. Сторонники XP приводят следующие доводы в защиту этой практики:
- Написание тестов помогает войти в рабочий ритм
- Придает уверенность в работоспособности кода.
- Дает запас прочности при дальнейшей интеграции или изменениях кода.
Согласен, вхождение в рабочий ритм — благородная задача. Уверенность в работоспособности — тоже хорошо. Но «уверенности в работоспособности» я предпочитаю действительно работоспособный код. Пусть даже при этом я не совсем «уверен».
Ключевой фактор при оценке перспективности любого метода — стоимость проекта. Дополнительная работа по созданию тестов, их кодированию и проверке результатов вносит существенный вклад в общую стоимость проекта. И то, что продукт окажется более качественным не всегда перевешивает то, что он будет существенно дороже.
Известно, что продукт оптимальный по набору бюджет/функциональность/качество получается при применении различных способов обеспечения качества. Бездумное применение тотального модульного тестирования почти гарантированно приведет к получению неоптимального продукта. И никакие «запасы прочности» и «быстрый вход в рабочий ритм» не спасут проект от провала.
На мой взгляд, модульное тестирование оправдано, если оно:
- Снижает время на отладку
- Дает возможность поиска ошибок с меньшими затратами, нежели при других подходах
- Дает возможность дешевого поиска ошибок при изменениях кода в дальнейшем
Суммарный выигрыш от применения модульных тестов должен быть больше, чем затраты на их создание и поддержание в актуальном состоянии.
Если в результате исправления ошибок интеграции меняется исходный код, в нем с большой вероятностью появляются ошибки. Если в результате добавления новой функциональности меняется исходный код, в нем с большой вероятностью появляются ошибки. И искать их лучше с помощью ранее созданных модульных тестов.
Цель модульного тестирования:
Получение работоспособного кода с наименьшими затратами. И его применение оправдано тогда и только тогда, когда оно дает больший эффект, нежели другие методы.
Отсюда следует несколько выводов:
- Нет смысла писать тесты на весь код. Некоторые ошибки проще найти на более поздних стадиях. Так, например, для ООП данное правило может звучать так: нет смысла писать тесты на класс, который используется только одним классом. Эффективней написать тесты на вызывающий класс и создать тесты тестирующие все участки кода.
- Писать тесты для кода потенциально подверженного изменениям более выгодно, чем для кода, изменение которого не предполагается. Сложная логика меняется чаще, чем простая. Следовательно, в первую очередь имеет смысл писать модульные тесты на сложную логику. А на простую логику писать позднее или вообще тестировать другими методами.
- Для того чтобы как можно реже изменять тесты следует хорошо планировать интерфейсы. То же самое можно сказать и применительно к написанию исходного кода. Действительно, создание хорошей архитектуры часто определяет дальнейший ход проекта. И есть оптимум, на каком этапе архитектура «достаточно хороша». Все так, но я хочу сказать о другом:
Если в проекте применяется модульное тестирование, то тщательное планирование интерфейсов становится более выгодным. Внедрению модульного тестирования должно предшествовать внедрение планирования интерфейсов.
Планирование тестов
Первый вопрос, который встает перед нами: «Сколько нужно тестов». Ответ, который часто дается: тестов должно быть столько, чтобы не осталось неоттестированных участков. Можно даже ввести формальное правило:
Код с не оттестированными участками не может быть опубликован
Проблема в том, что хотя неоттестированный код почти наверняка неработоспособен, но полное покрытие не гарантирует работоспособности. Написание тестов исходя только из уже существующего кода только для того, чтобы иметь стопроцентное покрытие кода тестами — порочная практика. Такой подход со всей неизбежностью приведет к существованию оттестированного, но неработоспособного кода. Кроме того, метод белового ящика, как правило, приводит к созданию позитивных тестов. А ошибки, как правило, находятся негативными тестами. В тестировании вопрос «Как я могу сломать?» гораздо эффективней вопроса «Как я могу подтвердить правильность?». Это наглядно демонстрирует статья 61 тест, который потряс программу.
В первую очередь тесты должны соответствовать не коду, а требованиям. Правило, которое следует применять:
Тесты должны базироваться на спецификации.
Пример такого подхода можно посмотреть в статье Тривиальная задача.
Один из эффективных инструментов, для определения полноты тестового набора — матрица покрытия.
На каждое требование должен быть, как минимум, один тест. Неважно, ручной или автоматический.
При подготовке тестового набора рекомендую начать с простого позитивного теста. Затраты на его создание минимальны. Да вероятность создания кода, не работающего в штатном режиме, гораздо меньше, чем отсутствие обработки исключительных ситуаций. Но исключительные условия в работе программы редки. Как правило, все работает в штатном режиме. Тесты на обработку некорректных условий, находят ошибки гораздо чаще, но если выяснится, что программа не обрабатывает штатные ситуации, то она просто никому не нужна.
Простой позитивный тест нужен т.к. несмотря на малую вероятность нахождения ошибки, цена пропущенной ошибки чрезмерно высока.
Последующие тесты должны создаваться при помощи формальных методик тестирования. Таких как, классы эквивалентности, исследование граничных условий, метод ортогональных матриц и т.д.. Тестирование накопило довольно много приемов подготовки тестов и если эти приемы создавались, то видимо было зачем.
Последнюю проверку полноты тестового набора следует проводить с помощью формальной метрики «Code Coverage». Она показывает неполноту тестового набора. И дальнейшие тесты можно писать на основании анализа неоттестированных участков.
Наиболее эффективный способ создания тестового набора — совместное использование методов черного и белого ящиков.
Распределение обязанностей
Где-то я читал следующую фразу: «Попросите программиста составить для вас (тестера) план тестов». А потом тестер будет кодировать тесты. Генеральный директор рисовать дизайн, а администратор баз данных писать руководство пользователя. Не очень воодушевляющая картина.
Кодировать модульные тесты проще всего программисту, который пишет исходный код. Но перед этим их нужно придумать. Кодирование тестов и разработка тестовых сценариев это две разные задачи. И для последней нужны навыки дизайнера сценариев. Если программист ими обладает, то все прекрасно. Если нет, то имеет смысл поручить это тестировщику. Нужно просто четко определить решаемые задачи и навыки, необходимые для их решения.
| Задача | Требуемые навыки | Роль |
|---|---|---|
| Определение методов обеспечения качества ПО | Отличное знание теории тестрования | Ведущий тестировщик проекта |
| Создание тестов | Хорошее знание методов тестирования | Дизайнер тестовых сценариев |
| Кодирование тестов | Средние навыки программирования | Программист автоматических тестов |
| Выполнение тестов | Знание среды выполнения тестов | Тестер |
Вполне возможно, что роль ведущего тестировщика проекта будет выполнять аналитик или менеджер проекта, роль дизайнер тестовых сценариев — программист. А может быть и так, что все эти роли будет выполнять тестировщик.
Не важно кто конкретно будет выполнять работу, и как будет называться должность. Главное, чтобы сотрудник обладал необходимыми навыками.
Модульное тестирование

При подходе “черного ящика” тестировщики не проводят модульное тестирование. Их главная цель – проверить приложение на соответствие требованиям, не вдаваясь в детали реализации.
Но, будучи любопытным или нестандартно мыслящим человеком, задумывались ли вы когда-нибудь о том, как разработчики тестируют свой код? Какой метод они используют для тестирования, прежде чем передать его QA? Как важно тестирование для разработчиков в agile-процессе?
Ответ на все это – unit-тестирование. Я хочу рассказать вам о важности unit (модульного) тестирования, чтобы команды разработчиков и тестировщиков могли работать более слаженно для разработки, тестирования и релиза качественного приложения.
Кто знает, возможно, в будущем некоторые из вас даже перейдут на тестирование “белого ящика” и будут самостоятельно использовать эти методы проверки и улучшения кода!
Что такое unit-тестирование?
Unit-тестирование – это не новая концепция. Оно существует еще с самого начала программирования. Обычно разработчики и иногда тестировщики, использующие подход “белого ящика”, пишут модульные тесты для улучшения качества кода путем проверки каждой единицы кода, используемой для реализации функциональных требований (также известная как разработка на основе тестов (TDD) или test-first разработка).
Большинство из нас знают классическое определение: “Unit-тестирование – это метод проверки самого маленького тестируемого фрагмента кода на соответствие его цели”. Если цель или требование не выполняются, значит, unit-тестирование провалено.
Простыми словами, это означает – написание части кода (модульного теста) для проверки кода (модуля), написанного для реализации требований.
Unit-тестирование в SDLC

Для unit-тестирования разработчики используют ручные или автоматизированные тесты, чтобы убедиться, что каждый блок ПО соответствует требованиям заказчика. Таким блоком может быть отдельная функция, объект, метод, процедура или модуль в тестируемом приложении.
Написание unit-тестов для тестирования отдельных единиц упрощает написание системных тестов, поскольку все единицы при этом собираются вместе. При разработке ПО это делается в качестве первого уровеня тестирования.
Важность написания unit-тестов
Unit-тестирование используется для разработки надежных компонентов ПО, которые помогают поддерживать код и устранять проблемы в его отдельных блоках. Все мы знаем, как важно находить и устранять дефекты на ранних стадиях цикла разработки программного обеспечения. Тестирование служит той же цели.
Оно является неотъемлемой частью agile-процесса разработки ПО. При ночной сборке должен запускаться набор модульных тестов и генерироваться отчет. Если какой-либо из unit-тестов не прошел, то команда QA не должна принимать эту сборку для проверки.
Если мы сделаем это стандартным процессом, многие дефекты будут отлавливаться на ранних стадиях разработки, что позволит сэкономить много времени на тестирование.
Я знаю, что многие разработчики ненавидят писать unit-тесты. Они либо игнорируют, либо пишут плохие unit-тесты из-за жесткого графика или недостаточной серьезности (да, они пишут пустые unit-тесты, поэтому 100% из них проходят успешно ;)). Важно писать хорошие модульные тесты или не писать их вообще. Еще важнее предоставить достаточно времени и благоприятную среду для получения от них реальной пользы.
Методы unit-тестирования
Оно может быть выполнено двумя способами:
- Ручное тестирование
- Автоматизированное тестирование
При ручном тестировании тестировщик вручную выполняет тест-кейсы без использования каких-либо средств автоматизации. Здесь каждый этап теста выполняется вручную. Такое тестирование утомительно, особенно для тестов, которые повторяются и требуют больших усилий для создания и выполнения тест-кейсов. Ручное тестирование не требует знания какого-либо инструмента автоматизации.
Фактом является то, что 100% автоматизация невозможна, поэтому определенный процент тест-кейсов всегда будет выполняться вручную.
В автоматизированном тестировании используются специальные инструменты для автоматизации тест-кейсов. Инструмент автоматизации может записать и сохранить ваш тест, и он может быть воспроизведен столько раз, сколько необходимо, без дальнейшего вмешательства человека.
Эти инструменты могут даже вводить тестовые данные в тестируемую систему, а также сравнивать ожидаемые результаты с фактическими и автоматически генерировать отчеты. Однако первоначальные затраты на создание средств автоматизации тестирования досточно высоки.
Техники unit-тестирования
1. Тестирование “белого ящика”:
При тестировании “белого ящика” тестировщик знаком с внутренней структурой ПО, включая код, и может проверить его на соответствие требованиям проекта. Тестирование “белого ящика” также известно как “прозрачное тестирование”.

2. Тестирование “черного ящика”:
При тестировании “черного ящика” тестировщик не знает ни внутренней структуры, ни имеет доступа к коду ПО.

3. Тестирование “серого ящика”:
Это также называется “полупрозрачным тестированием”, которое означает, что тестировщики лишь частично осведомлены о внутренней структуре, функциях и дизайне, а также о требованиях к проекту.
Отладка осуществляется путем фактического ввода данных с frontend для получения точных данных с backend. Таким образом, “серый ящик” считается комбинацией методов тестирования “черного” и “белого” ящиков.

Тестирование “серого ящика” охватывает следующие виды тестирования:
- Тестирование по матрице требований.
- Регрессионное тестирование.
Преимущества unit-тестирования
- Процесс становится гибким: Чтобы добавить новые функции или возможности в существующее ПО, необходимо внести изменения в старый код. Но внесение изменений в уже протестированный код может быть рискованным и дорогостоящим.
- Улучшается качество кода: Качество кода автоматически улучшается, когда проводится модульное тестирование. Ошибки, выявленные в ходе такого тестирования, исправляются до того, как они будут отправлены на этап интеграционного тестирования. Это приводит к надежности проектирования и разработки, поскольку разработчики пишут тест-кейсы, предварительно разобравшись в спецификациях.
- Раннее обнаружение ошибок: Выполняя unit-тесты, разработчики обнаруживают ошибки на ранних этапах жизненного цикла разработки ПО и устраняют их. Это касается как недостатков или отсутствующих частей в спецификации, так и ошибок в реализации программиста.
- Более легкие изменения и упрощенная интеграция: Проведение unit-тестирования облегчает разработчику реструктуризацию кода, внесение изменений и сопровождение кода. Это также значительно упрощает тестирование кода после интеграции. Исправление проблемы при unit-тестировании может застраховать от ряда других дефектов, возникающих на последующих этапах разработки и тестирования.
- Доступность документации: Разработчики, которые изучают функциональность на более поздних этапах, могут обратиться к документации по unit-тестированию и легко найти интерфейс модульного теста – затем быстро и легко исправить работу.
- Простой процесс отладки: Это помогает упростить процесс отладки. Если тест не прошел на каком-либо этапе, код необходимо отлаживать, иначе процесс можно продолжать без каких-либо препятствий.
- Более низкая стоимость: Когда ошибки обнаруживаются и устраняются во время unit-тестирования, снижается стоимость и время разработки. Без такого тестирования, если те же ошибки будут обнаружены на более позднем этапе после интеграции кода, их будет сложнее отследить и устранить, что приведет к увеличению времени разработки и, следовательно, стоимости.
- С помощью unit-тестов можно проверить полноту кода: Это более полезно в agile-процессах. Тестировщики не получают функциональные сборки для тестирования до завершения интеграции. Полноту кода нельзя обосновать, показав, что вы написали и проверили код. Однако выполнение unit-тестов может продемонстрировать завершенность кода.
- Экономия времени разработки: Завершение реализации какой-либо функциональности может занять больше времени, но благодаря меньшему количеству ошибок при системном и приемочном тестировании можно сэкономить общее время разработки.
- Покрытие кода может быть измерено.
Цикл модульного тестирования

Что делает хороший unit-тест?
Я не тот человек, который может с точностью сказать, что делает хороший unit-тест, но на основе моих наблюдений за различными проектами я могу назвать его характеристики. Плохие модульные тесты не добавляют ценности проекту. Наоборот, стоимость проекта значительно возрастает из-за написания и управления плохими модульными тестами.
Как писать хорошие unit-тесты?
- Unit-тесты должны быть написаны для проверки отдельной единицы кода, а не интеграции.
- Небольшие и изолированные unit-тесты с четким именованием облегчат их написание и сопровождение.
- Изменения в другой части ПО не должны влиять на unit-тесты, если они изолированы и написаны для конкретной части кода.
- Они должны выполняться быстро.
- Unit-тесты должны быть многократно используемыми.
Фреймворки для unit-тестирования
Фреймворки для unit-тестирования в основном используются для того, чтобы помочь быстро и легко написать модульные тесты. Большинство языков программирования не поддерживают модульное тестирование встроенным компилятором. Чтобы сделать unit-тестирование еще более увлекательным, можно использовать сторонние инструменты с открытым исходным кодом и платные инструменты.
Список популярных инструментов unit-тестирования для различных языков программирования:
- Java framework – JUnit
- PHP-фреймворк – PHPUnit
- Фреймворки C++ – UnitTest++ и Google C++
- Фреймворк .NET – NUnit
- Фреймворк Python – pytest
Частые заблуждения о unit-тестировании:
- Написание кода с применением unit-тестов занимает больше времени, которого у нас и так нет – в действительности, это сэкономит время разработки в долгосрочной перспективе.
- Unit-тестирование поможет найти все ошибки – не поможет, поскольку цель unit-тестирования заключается не в поиске ошибок, а в разработке надежных программных компонентов, которые будут иметь меньше дефектов на последующих этапах SDLC.
- 100% покрытие кода означает 100% покрытие тестов – это не гарантирует, что код не содержит ошибок.
Как создавать и выполнять unit-тесты?
Хорошее модульное тестирование может состоять из 3 основных частей.
- Написать код unit-теста.
- Запустите код модульного тестирования, чтобы проверить, соответствует ли он системным требованиям.
- Выполнить программный код, чтобы проверить его на наличие дефектов и убедиться, что код соответствует системным требованиям.
После выполнения вышеуказанных трех шагов, если код отрабатывает корректно, то считается, что модульный тест пройден. Если он не соответствует системным требованиям, то тест провален. В этом случае разработчику необходимо перепроверить и исправить код.
В некоторых случаях для более точного тестирования необходимо разделить код.
Лучшие практики
Чтобы создать наиболее качественный код, учитывайте следующие моменты во время тестирования:
- Код должен быть стабильным: Бывают случаи, когда тест проваливается или в худшем случае вообще не выполняется, если код сломан.
- Понятным и полезным: Код должен быть простым для понимания. Это облегчает написание кода для разработчика, и даже другие разработчики, которые будут работать над кодом впоследствии, смогут легко его отладить.
- Должен быть один случай: Тесты, которые определяют несколько тест-кейсов сразными входными даными в одной функции, сложны в работе. Таким образом, написание кода для одного тест-кейса является лучшей практикой, что делает код более простым для понимания и отладки.
- Автоматизируйте запуск тестов: Разработчики должны позаботиться о том, чтобы тест выполнялся автоматически. Это должно быть в рамках непрерывного процесса доставки или интеграции (CI/CD).
Другие моменты, которые следует иметь в виду:
- Вместо того чтобы создавать тест-кейсы для всех существующих входных данных, сосредоточьтесь на тесте, который влияет на поведение системы.
- Существует вероятность повторения ошибки из-за кэша браузера.
- Тест-кейсы не должны быть взаимозависимыми.
- Обращайте внимание на циклы и условия в коде.
- Планируйте тест-кейсы чаще.
Заключение
Unit-тестирование становится необходимым тогда, когда требуется тестировать каждую функцию отдельно. Гораздо разумнее обнаружить и исправить ошибки во время такого тестирования и сэкономить время и затраты, чем находить их на более поздней стадии разработки ПО.
Несмотря на то, что этот метод дает много преимуществ, существуют и ограничения, связанные с его использованием. На протяжении всего процесса разработки ПО необходимы строгая дисциплина и последовательность, только это поможет преодолеть ограничения и получить желаемые преимущества.
Ваши комментарии приветствуются!
Как тестировщик, использующий подход “черного ящика”, каковы ваши наблюдения о unit-тестировании в вашей команде? Есть ли у кого-нибудь идеи для улучшения unit-тестирования?
Похожие записи:
- Мутационное тестирование
- Что такое DDT?
- Что такое нефункциональное тестирование?
- Как проводить backend тестирование
Уровни тестирования: компонентное тестирование, модульное тестирование

В практике тестирования новых программных продуктов, мобильных приложений и различных софтов существует такое понятие, как Test Pyramid, что означает разделение и группировку всех компонентов софта по определенным категориям (уровням).
Данные уровни тестирования применяются буквально повсеместно, начиная от момента прописывания кода и до создания конечного интерфейса.
Что такое уровни тестирования?
Как мы уже определили, уровни тестирования в образном понимании представляют собой пирамиду. И каждый новый уровень комплементарен с нижним и последующим. Каждый отдельный уровень имеет четкую характеристику и перечень тех параметров, которые подлежат тестированию в конкретный период времени. Важно усвоить, что именно поэтапное тестирование, проходящее по всем уровням, дает наибольший результат, который в дальнейшем приведет к положительному отклику пользователей. В образовательных курсах от Test Pro, мы детально изучаем все уровни и методы тестирования продуктов, а также обучаем наших студентов применять полученные знания на практике. Давайте чуть более детально рассмотрим первые (базовые) уровни тестирования: модульное тестирование и компонентное тестирование.

Модульное тестирование
Модульное тестирование (или Unit test) — базовый уровень “пирамиды”. Модульные тесты проводят для отдельно взятых элементов или подпрограмм в коде. Как правило, юнит-тесты проводятся непосредственно разработчиками, позволяя на самом начальном этапе определить ошибки кода, возможные дефекты алгоритмов, что в дальнейшем может привести к некорректной “отдаче” интерфейса, ошибкам в модулях и низкой производительности.
Модульное тестирование имеет несколько преимуществ, среди которых можно выделить следующие:
- Простота выполнения. Гораздо проще проводить тестирование на начальном этапе разработки под каждый отдельный модуль, нежели в дальнейшем тестировать весь продукт с нуля;
- Возможность многоразового использования. Правильно прописанный тест можно будет использовать в дальнейшем;
- Информативность. Unit-тест позволяет оценить API, функционал модулей и другие характеристики.
Также модульное тестирование позволяет разработчикам продолжать работу над другими компонентами приложений в то время, пока тестируется изолированный модуль.
Компонентное тестирование
Компонентное тестирование (или Component testing) — следующий более высокий уровень тестирования ІТ-продуктов. Он предполагает проведение тестирования для единиц (юнитов), объединенных в компоненты. При этом каждый из этих компонентов может тестироваться в индивидуальном порядке.
Компонентное тестирование — один из методов, который применяется специалистами QA при тестировании “Черного ящика”. Component testing может также иметь разные уровни. Например, можно провести Small Component testing (c изоляцией каждого отдельного компонента) или Large Component testing (без изоляции компонентов).
UNIT-тестирование, или как стать специалистом в сфере IT
Unit тестирование, также как и компонентный анализ, являются базовыми способами тестирования большинства современных IT-продуктов, приложений и различных софтов. Этап тестирования необходим для своевременного выявления и устранения ошибок в коде и облегчения дальнейшей работы QA-инженеров и SDET-специалистов.
Проведение многоэтапного тестирования — очень ответственное задание, которое должен уметь использовать в своей работе каждый высококвалифицированный специалист. Именно поэтому, преподавательский состав Test Pro уделяет особое внимание подготовке своих студентов по данному направлению.
Наши курсы созданы с учетом специфики и особенностей работы тестировщиков, инженеров и разработчиков, что позволяет дать студентам самую прочную базу знаний, которую они сразу смогут применять на практике.
Присоединяйтесь к новому набору на один из курсов от команды Test Pro через формы Apply или Book a call, и уже через 9 недель вы сможете найти работу мечты, которая позволит развиваться и ежегодно увеличивать свой доход.
Часто задаваемые вопросы
По сути, эти уровни тестирования весьма схожи между собой при единственном небольшом отличии — объеме исследуемых единиц. Т.е. компонентное тестирование дает возможность оценить уже готовые узлы и алгоритмы, а модульное — начальные единицы кода.
Это разновидность модульного тестирования, характеризующаяся тем, что в данном конкретном случае специалист изолирует только один юнит для проверки. Также встречаются еще “социальные” юнит-тесты, в ходе которых изолируется сразу несколько позиций.
Получить информацию о наших курсах вы можете в соответствующем разделе на нашем сайте или связавшись с одним из менеджеров Test Pro в онлайн-режиме.
В среднем программа обучения занимает 9 недель. Но может также быть индивидуально подстроена под студента.
Юнит-тестирование для чайников
Даже если вы никогда в жизни не думали, что занимаетесь тестированием, вы это делаете. Вы собираете свое приложение, нажимаете кнопку и проверяете, соответствует ли полученный результат вашим ожиданиям. Достаточно часто в приложении можно встретить формочки с кнопкой “Test it” или классы с названием TestController или MyServiceTestClient.

То что вы делаете, называется интеграционным тестированием. Современные приложения достаточно сложны и содержат множество зависимостей. Интеграционное тестирование проверяет, что несколько компонентов системы работают вместе правильно.
Оно выполняет свою задачу, но сложно для автоматизации. Как правило, тесты требуют, чтобы вся или почти вся система была развернута и сконфигурирована на машине, на которой они выполняются. Предположим, что вы разрабатываете web-приложение с UI и веб-сервисами. Минимальная комплектация, которая вам потребуется: браузер, веб-сервер, правильно настроенные веб-сервисы и база данных. На практике все еще сложнее. Разворачивать всё это на билд-сервере и всех машинах разработчиков?
We need to go deeper

Давайте сначала спустимся на предыдущий уровень и убедимся, что наши компоненты работают правильно по-отдельности.
Обратимся к википедии:
Модульное тестирование, или юнит-тестирование (англ. unit testing) — процесс в программировании, позволяющий проверить на корректность отдельные модули исходного кода программы.
Идея состоит в том, чтобы писать тесты для каждой нетривиальной функции или метода. Это позволяет достаточно быстро проверить, не привело ли очередное изменение кода к регрессии, то есть к появлению ошибок в уже оттестированных местах программы, а также облегчает обнаружение и устранение таких ошибок.

Таким образом, юнит-тестирование – это первый бастион на борьбе с багами. За ним еще интеграционное, приемочное и, наконец, ручное тестирование, в том числе «свободный поиск».
Нужно ли все это вам? С моей точки зрения ответ: «не всегда».
Не нужно писать тесты, если
- Вы делаете простой сайт-визитку из 5 статических html-страниц и с одной формой отправки письма. На этом заказчик, скорее всего, успокоится, ничего большего ему не нужно. Здесь нет никакой особенной логики, быстрее просто все проверить «руками»
- Вы занимаетесь рекламным сайтом/простыми флеш-играми или баннерами – сложная верстка/анимация или большой объем статики. Никакой логики нет, только представление
- Вы делаете проект для выставки. Срок – от двух недель до месяца, ваша система – комбинация железа и софта, в начале проекта не до конца известно, что именно должно получиться в конце. Софт будет работать 1-2 дня на выставке
- Вы всегда пишете код без ошибок, обладаете идеальной памятью и даром предвидения. Ваш код настолько крут, что изменяет себя сам, вслед за требованиями клиента. Иногда код объясняет клиенту, что его требования —
говне нужно реализовывать
В первых трех случаях по объективным причинам (сжатые сроки, бюджеты, размытые цели или очень простые требования) вы не получите выигрыша от написания тестов.
Последний случай рассмотрим отдельно. Я знаю только одного такого человека, и если вы не узнали себя на фото ниже, то у меня для вас плохие новости.

Любой долгосрочный проект без надлежащего покрытия тестами обречен рано или поздно быть переписанным с нуля

- Без покрытия тестами. Обычно такие системы сопровождаются спагетти-кодом и уволившимися ведущими разработчиками. Никто в компании не знает, как именно все это работает. Да и что оно в конечном итоге должно делать, сотрудники представляют весьма отдаленно.
- С тестами, которые никто не запускает и не поддерживает. Тесты в системе есть, но что они тестируют, и какой от них ожидается результат, неизвестно. Ситуация уже лучше. Присутствует какая-никакая архитектура, есть понимание, что такое слабая связанность. Можно отыскать некоторые документы. Скорее всего, в компании еще работает главный разработчик системы, который держит в голове особенности и хитросплетения кода.
- С серьезным покрытием. Все тесты проходят. Если тесты в проекте действительно запускаются, то их много. Гораздо больше, чем в системах из предыдущей группы. И теперь каждый из них – атомарный: один тест проверяет только одну вещь. Тест является спецификацией метода класса, контрактом: какие входные параметры ожидает этот метод, и что остальные компоненты системы ждут от него на выходе. Таких систем гораздо меньше. В них присутствует актуальная спецификация. Текста немного: обычно пара страниц, с описанием основных фич, схем серверов и getting started guide’ом. В этом случае проект не зависит от людей. Разработчики могут приходить и уходить. Система надежно протестирована и сама рассказывает о себе путем тестов.
Проекты первого типа – крепкий орешек, с ними работать тяжелее всего. Обычно их рефакторинг по стоимости равен или превышает переписывание с нуля.
Почему есть проекты второго типа?
Коллеги из ScrumTrek уверяют, что всему виной темная сторона кода и властелин Дарт Автотестиус. Я убежден, что это очень близко к правде. Бездумное написание тестов не только не помогает, но вредит проекту. Если раньше у вас был один некачественный продукт, то написав тесты, не разобравшись в этой теме, вы получите два. И удвоенное время на сопровождение и поддержку.
Для того чтобы темная сторона кода не взяла верх, нужно придерживаться следующих основных правил.
Ваши тесты должны:
- Быть достоверными
- Не зависеть от окружения, на котором они выполняются
- Легко поддерживаться
- Легко читаться и быть простыми для понимания (даже новый разработчик должен понять что именно тестируется)
- Соблюдать единую конвенцию именования
- Запускаться регулярно в автоматическом режиме
Выберите логическое расположение тестов в вашей VCS
Только так. Ваши тесты должны быть частью контроля версий. В зависимости от типа вашего решения, они могут быть организованы по-разному. Общая рекомендация: если приложение монолитное, положите все тесты в папку Tests; если у вас много разных компонентов, храните тесты в папке каждого компонента.
Выберите способ именования проектов с тестами
Одна из лучших практик: добавьте к каждому проекту его собственный тестовый проект.
У вас есть части системы .Core, .Bl и .Web? Добавьте еще .Core.Tests, .Bl.Tests и .Web.Tests.
У такого способа именования есть дополнительный сайд-эффект. Вы сможете использовать паттерн *.Tests.dll для запуска тестов на билд-сервере.
Используйте такой же способ именования для тестовых классов
У вас есть класс ProblemResolver? Добавьте в тестовый проект ProblemResolverTests. Каждый тестирующий класс должен тестировать только одну сущность. Иначе вы очень быстро скатитесь в унылое го во второй тип проектов (с тестами, которые никто не запускает).
Выберите «говорящий» способ именования методов тестирующих классов
TestLogin – не самое лучшее название метода. Что именно тестируется? Каковы входные параметры? Могут ли возникать ошибки и исключительные ситуации?
На мой взгляд, лучший способ именования методов такой: [Тестируемый метод]_[Сценарий]_[Ожидаемое поведение].
Предположим, что у нас есть класс Calculator, а у него есть метод Sum, который (привет, Кэп!) должен складывать два числа.
В этом случае наш тестирующий класс будет выглядеть так:
сlass CalculatorTests < public void Sum_2Plus5_7Returned() < // … >>
Такая запись понятна без объяснений. Это спецификация к вашему коду.
Выберите тестовый фреймворк, который подходит вам
Вне зависимости от платформы не стоит писать велосипеды. Я видел много проектов, в которых автоматические тесты (в основном, не юнит, а приемочные) запускались из консольного приложения. Не надо этого делать, все уже сделано за вас.
Уделите чуть больше внимания обзору фреймворков. Например, многие .NET разработчики используют MsTest только потому, что он входит в поставку студии. Мне гораздо больше по душе NUnit. Он не создает лишних папок с результатами тестов и имеет поддержку параметризированного тестирования. Я могу так же легко запускать мои тесты на NUnit с помощью Решарпера. Кому-то понравится элегантность xUnit’а: конструктор вместо атрибутов инициализации, реализация IDisposable как TearDown.
Что тестировать, а что – нет?

Одни говорят о необходимости покрытия кода на 100%, другие считают это лишней тратой ресурсов.
Мне нравится такой подход: расчертите лист бумаги по оси X и Y, где X – алгоритмическая сложность, а Y – количество зависимостей. Ваш код можно разделить на 4 группы.
Рассмотрим сначала экстремальные случаи: простой код без зависимостей и сложный код с большим количеством зависимостей.
- Простой код без зависимостей. Скорее всего здесь и так все ясно. Его можно не тестировать.
- Сложный код с большим количеством зависимостей. Хм, если у вас есть такой код, тут пахнет God Object’ом и сильной связностью. Скорее всего, неплохо будет провести рефакторинг. Мы не станем покрывать этот код юнит-тестами, потому что перепишем его, а значит, у нас изменятся сигнатуры методов и появятся новые классы. Так зачем писать тесты, которые придется выбросить? Хочу оговориться, что для проведения такого рода рефакторинга нам все же нужно тестирование, но лучше воспользоваться более высокоуровневыми приемочными тестами. Мы рассмотрим этот случай отдельно.
- Cложный код без зависимостей. Это некие алгоритмы или бизнес-логика. Отлично, это важные части системы, тестируем их.
- Не очень сложный код с зависимостями. Этот код связывает между собой разные компоненты. Тесты важны, чтобы уточнить, как именно должно происходить взаимодействие. Причина потери Mars Climate Orbiter 23 сентября 1999 года заключалась в программно-человеческой ошибке: одно подразделение проекта считало «в дюймах», а другое – «в метрах», и прояснили это уже после потери аппарата. Результат мог быть другим, если бы команды протестировали «швы» приложения.
Придерживайтесь единого стиля написания тела теста
Отлично зарекомендовал себя подход AAA (arrange, act, assert) . Вернемся к примеру с калькулятором:
class CalculatorTests < public void Sum_2Plus5_7Returned() < // arrange var calc = new Calculator(); // act var res = calc.Sum(2,5); // assert Assert.AreEqual(7, res); >>
Такая форма записи гораздо легче читается, чем
class CalculatorTests < public void Sum_2Plus5_7Returned() < Assert.AreEqual(7, new Calculator().sum(2,5)); >>
А значит, этот код проще поддерживать.
Тестируйте одну вещь за один раз
Каждый тест должен проверять только одну вещь. Если процесс слишком сложен (например, покупка в интернет магазине), разделите его на несколько частей и протестируйте их отдельно.
Если вы не будете придерживаться этого правила, ваши тесты станут нечитаемыми, и вскоре вам окажется очень сложно их поддерживать.
Борьба с зависимостями
До сих пор мы тестировали калькулятор. У него совсем нет зависимостей. В современных бизнес-приложениях количество таких классов, к сожалению, мало.
Рассмотрим такой пример.
public class AccountManagementController : BaseAdministrationController < #region Vars private readonly IOrderManager _orderManager; private readonly IAccountData _accountData; private readonly IUserManager _userManager; private readonly FilterParam _disabledAccountsFilter; #endregion public AccountManagementController() < _oms = OrderManagerFactory.GetOrderManager(); _accountData = _ orderManager.GetComponent(); _userManager = UserManagerFactory.Get(); _disabledAccountsFilter = new FilterParam("Enabled", Expression.Eq, true); > >
Фабрика в этом примере берет данные о конкретной реализации AccountData из файла конфигурации, что нас абсолютно не устраивает. Мы же не хотим поддерживать зоопарк файлов *.config. Более того, настоящие реализации могут зависеть от базы данных. Если мы продолжим в том же духе, то перестанем тестировать только методы контроллера и начнем вместе с ними тестировать другие компоненты системы. Как мы помним, это называется интеграционным тестированием.
Чтобы не тестировать все вместе, мы подсунем фальшивую реализацию (fake).
Перепишем наш класс так:
public class AccountManagementController : BaseAdministrationController < #region Vars private readonly IOrderManager _oms; private readonly IAccountData _accountData; private readonly IUserManager _userManager; private readonly FilterParam _disabledAccountsFilter; #endregion public AccountManagementController() < _oms = OrderManagerFactory.GetOrderManager(); _accountData = _oms.GetComponent(); _userManager = UserManagerFactory.Get(); _disabledAccountsFilter = new FilterParam("Enabled", Expression.Eq, true); > /// /// For testability /// /// /// public AccountManagementController( IAccountData accountData, IUserManager userManager) < _accountData = accountData; _userManager = userManager; _disabledAccountsFilter = new FilterParam("Enabled", Expression.Eq, true); >>
Теперь у контроллера появилась новая точка входа, и мы можем передать туда другие реализации интерфейсов.
Fakes: stubs & mocks
Мы переписали класс и теперь можем подсунуть контроллеру другие реализации зависимостей, которые не станут лезть в базу, смотреть конфиги и т.д. Словом, будут делать только то, что от них требуется. Разделяем и властвуем. Настоящие реализации мы должны протестировать отдельно в своих собственных тестовых классах. Сейчас мы тестируем только контроллер.
Выделяют два типа подделок: стабы (stubs) и моки (mock).
Часто эти понятия путают. Разница в том, что стаб ничего не проверяет, а лишь имитирует заданное состояние. А мок – это объект, у которого есть ожидания. Например, что данный метод класса должен быть вызван определенное число раз. Иными словами, ваш тест никогда не сломается из-за «стаба», а вот из-за мока может.
С технической точки зрения это значит, что используя стабы в Assert мы проверяем состояние тестируемого класса или результат выполненного метода. При использовании мока мы проверяем, соответствуют ли ожидания мока поведению тестируемого класса.
Стаб
[Test] public void LogIn_ExisingUser_HashReturned() < // Arrange OrderProcessor = Mock.Of(); OrderData = Mock.Of(); LayoutManager = Mock.Of(); NewsProvider = Mock.Of(); Service = new IosService( UserManager, AccountData, OrderProcessor, OrderData, LayoutManager, NewsProvider); // Act var hash = Service.LogIn("ValidUser", "Password"); // Assert Assert.That(!string.IsNullOrEmpty(hash)); >
Мок
[Test] public void Create_AddAccountToSpecificUser_AccountCreatedAndAddedToUser() < // Arrange var account = Mock.Of(); // Act _controller.Create(1, account); // Assert _accountData.Verify(m => m.CreateAccount(It.IsAny()), Times.Exactly(1)); _accountData.Verify(m => m.AddAccountToUser(It.IsAny(), It.IsAny()), Times.Once()); >
Тестирование состояния и тестирование поведения
Почему важно понимать, казалось бы, незначительную разницу между моками и стабами? Давайте представим, что нам нужно протестировать автоматическую систему полива. Можно подойти к этой задаче двумя способами:
Тестирование состояния
Запускаем цикл (12 часов). И через 12 часов проверяем, хорошо ли политы растения, достаточно ли воды, каково состояние почвы и т.д.
Тестирование взаимодействия
Установим датчики, которые будут засекать, когда полив начался и закончился, и сколько воды поступило из системы.
Стабы используются при тестировании состояния, а моки – взаимодействия. Лучше использовать не более одного мока на тест. Иначе с высокой вероятностью вы нарушите принцип «тестировать только одну вещь». При этом в одном тесте может быть сколько угодно стабов или же мок и стабы.
Изоляционные фреймвоки
- Велосипеды уже написаны до нас
- Многие интерфейсы не так просто реализовать с полпинка
- Наши самописные подделки могут содержать ошибки
- Это дополнительный код, который придется поддерживать
В примере выше я использовал фреймворк Moq для создания моков и стабов. Довольно распространен фреймворк Rhino Mocks. Оба фреймворка — бесплатные. На мой взгляд, они практически эквивалентны, но Moq субъективно удобнее.
На рынке есть также два коммерческих фреймворка: TypeMock Isolator и Microsoft Moles. На мой взгляд они обладают чрезмерными возможностями подменять невиртуальные и статические методы. Хотя при работе с унаследованным кодом это и может быть полезно, ниже я опишу, почему все-таки не советую заниматься подобными вещами.
Шоукейсы перечисленных изоляционных фреймворков можно посмотреть тут. А информацию по техническим аспектам работы с ними легко найти на Хабре.
Тестируемая архитектура
Вернемся к примеру с контроллером.
public AccountManagementController( IAccountData accountData, IUserManager userManager)
Здесь мы отделались «малой кровью». К сожалению, не всегда все бывает так просто. Давайте рассмотрим основные случаи, как мы можем внедрить зависимости:
Инъекция в конструктор
Добавляем дополнительный конструктор или заменяем текущий (зависит от того, как вы создаете объекты в вашем приложении, используете ли IOC-контейнер). Этим подходом мы воспользовались в примере выше.
Инъекция в фабрику
Setter можно дополнительно «спрятать» от основного приложения, если выделить интерфейс IUserManagerFactory и работать в продакшн-коде по интерфейсной ссылке.
public class UserManagerFactory < private IUserManager _instance; /// /// Get UserManager instance /// /// IUserManager with configuration from the configuration file public IUserManager Get() < return _instance ?? Get(UserConfigurationSection.GetSection()); >private IUserManager Get(UserConfigurationSection config) < return _instance ?? (_instance = Create(config)); >/// /// For testing purposes only! /// /// public void Set(IUserManager userManager) < _instance = userManager; >>
Подмена фабрики
Вы можете подменить всю фабрику целиком. Это потребует выделение интерфейса или создание виртуальной функции, создание объектов. После этого вы сможете переопределить фабричные методы так, чтобы они возвращали ваши подделки.
Переопределение локального фабричного метода
Если зависимости инстанцируются прямо в коде явным образом, то самый простой путь – выделить фабричный protected-метод CreateObjectName() и переопределить его в классе-наследнике. После этого тестируйте класс-наследник, а не ваш первоначально тестируемый класс.
Например, мы решили написать расширяемый калькулятор (со сложными действиями) и начали выделять новый слой абстракции.
public class Calculator < public double Multipy(double a, double b) < var multiplier = new Multiplier(); return multiplier.Execute(a, b); >> public interface IArithmetic < double Execute(double a, double b); >public class Multiplier : IArithmetic < public double Execute(double a, double b) < return a * b; >>
Мы не хотим тестировать класс Multiplier, для него будет отдельный тест. Перепишем код так:
public class Calculator < public double Multipy(double a, double b) < var multiplier = CreateMultiplier(); return multiplier.Execute(a, b); >protected virtual IArithmetic CreateMultiplier() < var multiplier = new Multiplier(); return multiplier; >> public class CalculatorUnderTest : Calculator < protected override IArithmetic CreateMultiplier() < return new FakeMultiplier(); >> public class FakeMultiplier : IArithmetic < public double Execute(double a, double b) < return 5; >>
Код намеренно упрощен, чтобы акцентировать внимание именно на иллюстрации способа. В реальной жизни вместо калькулятора, скорее всего, будут DataProvider’ы, UserManager’ы и другие сущности с гораздо более сложной логикой.
Тестируемая архитектура VS OOP
Многие разработчики начинают жаловаться, дескать «этот ваш тестируемый дизайн» нарушает инкапсуляцию, открывает слишком много. Я думаю, что существует только две причины, когда это может вас беспокоить:
Серьезные требования к безопасности
Это значит, что у вас серьезная криптография, бинарники упакованы, и все обвешано сертификатами.
Даже если так, скорее всего, вы сможете найти компромиссное решение. Например, в .NET вы можете использовать internal-методы и атрибут [InternalsVisibleTo], чтобы дать доступ к тестируемым методам из ваших тестовых сборок.
Производительность
Существует ряд задач, когда архитектурой приходится жертвовать в угоду производительности, и для кого-то это становится поводом отказаться от тестирования. В моей практике докинуть сервер/проапгрейдить железо всегда было дешевле, чем писать нетестируемый код. Если у вас есть критический участок, вероятно, стоит переписать его на более низком уровне. Ваше приложение на C#? Возможно, есть смысл собрать одну неуправляемую сборку на С++.
- Мыслите интерфейсами, а не классами, тогда вы всегда сможете легко подменять настоящие реализации подделками в тестовом коде
- Избегайте прямого инстанцирования объектов внутри методов с логикой. Используйте фабрики или dependency injection. В этом случае использование IOC-контейнера в проекте может сильно упростить вам работу.
- Избегайте прямого вызова статических методов
- Избегайте конструкторов, которые содержат логику: вам сложно будет это протестировать.
Работа с унаследованным кодом
Под «унаследованным» мы будем понимать код без тестов. Качество такого кода может быть разным. Несколько советов, как можно покрыть его тестами.
Архитектура тестируема
Нам повезло, прямых созданий классов и мясорубки нет, а принципы SOLID соблюдаются. Нет ничего проще – создаем тестовые проекты, и шаг за шагом покрываем приложение, используя принципы, описанные в статье. В крайнем случае, нам придется добавить пару сеттеров для фабрик и выделить несколько интерфейсов.
Архитектура не тестируема
У нас есть жесткие связи, костыли и прочие радости жизни. Нам предстоит рефакторинг. Как правильно проводить комплексный рефакторинг – тема, выходящая далеко за рамки этой статьи.
Стоит выделить основное правило. Если вы не меняете интерфейсов – все просто, методика идентична. А вот если вы задумали большие перемены, следует составить граф зависимостей и разбить ваш код на отдельные более мелкие подсистемы (надеюсь, что это возможно). В идеале должно получиться примерно так: ядро, модуль #1, модуль #2 и т.д.
После этого выберите жертву. Только не начинайте с ядра. Возьмите сначала что-то поменьше: то, что вы способны отрефакторить за разумное время. Покрывайте эту подсистему интеграционными и/или приемочными тестами. А когда закончите, сможете покрыть эту часть юнит-тестами. Рано или поздно, шаг за шагом, вы должны преуспеть.
Будьте готовы, что сделать это быстро скорее всего не получится. Вам придется проявить волевые качества.
Поддержка тестов

Не относитесь к своим тестам как к второсортному коду. Многие начинающие разработчики ошибочно полагают, что DRY, KISS и все остальное – это для продакшна. А в тестах допустимо все. Это не верно. Тесты – такой-же код. Разница только в том, что у тестов другая цель – обеспечить качество вашего приложения. Все принципы, применямые в разработке продакшн-кода могут и должны применяться при написании тестов.
Есть всего три причины, почему тест перестал проходить:
- Ошибка в продакшн-коде: это баг, его нужно завести в баг-трекере и починить.
- Баг в тесте: видимо, продакшн-код изменился, а тест написан с ошибкой (например, тестирует слишком много или не то, что было нужно). Возможно, что раньше он проходил ошибочно. Разберитесь и почините тест.
- Смена требований. Если требования изменились слишком сильно – тест должен упасть. Это правильно и нормально. Вам нужно разобраться с новыми требованиями и исправить тест. Или удалить, если он больше не актуален.
Уделяйте внимание поддержке ваших тестов, чините их вовремя, удаляйте дубликаты, выделяйте базовые классы и развивайте API тестов. Можно завести шаблонные базовые тестовые классы, которые обязывают реализовать набор тестов (например CRUD). Если делать это регулярно, то вскоре это не будет занимать много времени.
Как «измерить» прогресс
Для измерения успешности внедрения юнит-тестов в вашем проекте следует использовать две метрики:
- Количество багов в новых релизах (в т.ч. и регрессии)
- Покрытие кода
Первая показывает, есть ли у наших действий результат, или мы впустую расходуем время, которое могли бы потратить на фичи. Вторая – как много нам еще предстоит сделать.
- NCover
- dotTrace
- встроенный в студию Test Coverage
Test First?

Я умышленно не касался этой темы до самого конца. С моей точки зрения Test First – хорошая практика, обладающая рядом неоспоримых преимуществ. Однако, по тем или иным причинам, иногда я отступаю от этого правила и пишу тесты после того, как готов код.
На мой взгляд, «как писать тесты» гораздо важнее, чем «когда это делать». Делайте, как вам удобно, но не забывайте: если вы начинаете с тестов, то получаете архитектуру «в придачу». Если вы сначала пишете код, вам возможно, придется его менять, чтобы сделать тестируемым.
Почитать на тему
Отличную подборку ссылок и книг по теме можно найти в этой статье на Хабре. Особенно рекомендую книгу The Art of Unit Testing. Я читал первое издание. Оказывается, вышло уже и второе.
