Что означает модульное тестирование
Перейти к содержимому

Что означает модульное тестирование

  • автор:

Модульное тестирование: все, что нужно знать

модульное тестирование

Модульное тестирование (unit-тестирование)- это процесс тестирования, который позволяет проверить отдельные модули программного обеспечения на предмет их правильности работы. Это один из самых распространенных методов тестирования и является неотъемлемой частью процесса разработки программного обеспечения.

В этой статье мы рассмотрим основные принципы модульного тестирования, его преимущества и недостатки, а также расскажем, как его правильно применять в разработке ПО.

Основы модульного тестирования

1.1 Что такое модульное тестирование?
Модульное тестирование — это процесс проверки функциональности отдельных модулей программного обеспечения. Модуль — это независимый компонент программы, который может быть протестирован отдельно от других модулей.

1.2 Как работает модульное тестирование?
В модульном тестировании программисты создают тестовые сценарии для каждого модуля, которые проверяют корректность его работы. Если тест не проходит, программисты находят и исправляют ошибки до тех пор, пока тест не будет пройден успешно.

1.3 Какие инструменты используются для модульного тестирования?
Существует множество инструментов для модульного тестирования, таких как JUnit, NUnit, PHPUnit и другие. Они обеспечивают возможность создания тестовых сценариев и автоматического выполнения тестов.

Преимущества и недостатки модульного тестирования

2.1 Преимущества
Unit-тестированием предоставляет множество преимуществ, включая:

Уменьшение количества ошибок в коде;
Ускорение процесса разработки ПО;
Увеличение надежности программного обеспечения;
Упрощение отладки программного обеспечения;
Снижение затрат на тестирование.

2.2 Недостатки
Несмотря на множество преимуществ, модульное тестирование имеет и недостатки:

Невозможность проверить взаимодействие между модулями;
Невозможность проверить функциональность программы в целом;
Трудность в написании тестов для сложных модулей.

Как правильно применять модульное тестирование

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

3.2 Создание тестовых сценариев
После выбора модулей для тестирования необходимо создать тестовые сценарии. Тестовые сценарии должны покрывать все возможные варианты использования модуля, чтобы убедиться, что модуль работает корректно.

3.3 Автоматическое тестирование
Для обеспечения повторяемости тестов и ускорения процесса тестирования необходимо автоматизировать процесс тестирования. Для этого используются специальные инструменты для модульного тестирования.

3.4 Интеграция с другими методами тестирования
Модульное тестирование не может полностью заменить другие методы тестирования, такие как интеграционное тестирование или функциональное тестирование. Поэтому важно интегрировать модульное тестирование с другими методами тестирования, чтобы обеспечить полное покрытие тестами всего программного обеспечения.

модульное тестирование

Когда нужно проводить модульное тестирование

  1. При разработке критически важного программного обеспечения, которое обрабатывает большие объемы данных и высоконагруженных систем.
  2. Когда нужно проверить работу отдельных компонентов системы, которые могут быть протестированы независимо от других компонентов.
  3. При внесении изменений в код. Модульное тестирование позволяет быстро проверить, что изменения не привели к появлению новых ошибок или не нарушили работу других компонентов системы.
  4. Для обеспечения быстрого и эффективного процесса тестирования. Модульные тесты легко автоматизировать и запускать, что позволяет экономить время и сократить затраты на тестирование.

Когда не нужно проводить модульное тестирование

  1. Когда приложение имеет простую архитектуру и небольшой объем кода. В таких случаях модульное тестирование может быть слишком ресурсоемким и неоправданным.
  2. Когда приложение не предусматривает использования модульного тестирования в качестве метода тестирования. Например, если команда разработки не имеет достаточного опыта в создании модульных тестов или не обладает соответствующими инструментами для модульного тестирования.
  3. Когда необходимо провести тестирование в реальных условиях. Модульное тестирование проверяет работу отдельных компонентов системы, но не всей системы в целом. Если нужно проверить работу системы в реальных условиях, то лучше использовать другие методы тестирования, например, интеграционное тестирование или функциональное тестирование.

Важно понимать, что модульное тестирование является только одним из методов тестирования и не может полностью заменить другие методы тестирования. Лучшим подходом является использование модульного тестирования в сочетании с другими методами тестирования для обеспечения полного покрытия тестами всего программного обеспечения.

Примеры модульных тестов

Конечная цель unit-тестированием — проверить, что отдельные компоненты программного обеспечения (называемые модулями) работают корректно в изоляции от других компонентов системы. Вот несколько примеров модульных тестов для простой функциональности:

  1. Функция для нахождения суммы двух чисел:
pythonCopy codedef test_sum(): assert sum(2, 3) == 5 assert sum(-1, 5) == 4 assert sum(0, 0) == 0

Эти тесты проверяют, что функция sum корректно складывает два числа и возвращает правильный результат.

  1. Функция для проверки, является ли число простым:
pythonCopy codedef test_is_prime(): assert is_prime(2) == True assert is_prime(4) == False assertis_prime(11) == True

Эти тесты проверяют, что функция is_prime правильно определяет, является ли число простым.

  1. Функция для вычисления факториала числа:
pythonCopy codedef test_factorial(): assert factorial(0) == 1 assert factorial(1) == 1 assertfactorial(5) == 120

Эти тесты проверяют, что функция factorial правильно вычисляет факториал числа.

В каждом из этих примеров мы проверяем, что функции работают корректно и возвращают правильный результат. Если тесты проходят успешно, то можно с уверенностью сказать, что отдельные компоненты программного обеспечения (эти функции) работают корректно в изоляции от других компонентов системы.

Заключение

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

Все о тестировании и качестве ПО
  • Нефункциональное тестирование
  • Тестирование-QC-QA разбираемся в вопросе
  • Тестировщик может справиться лучше?
  • Тестирование чисел
  • Валидация и верификация

I believe in QA, все о тестировании

Октябрь 2023

Пн Вт Ср Чт Пт Сб Вс
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31

Больше о тестировании и качестве ПО

  • Нефункциональное тестирование
  • Системное тестирование
  • Валидация и верификация
  • Формальное тестирование
  • Тестировщик может справиться лучше?
  • Тестирование-QC-QA разбираемся в вопросе

Что означает модульное тестирование

Модульное тестирование: типы и инструменты

09 июня 2020
Модульное тестирование: что это?
Типы, инструменты.
Что такое модульное тестирование? Зачем оно нужно? Примеры, подходы, стратегия и методологии.

В этой статье вы найдете следующую информацию:

  • Что такое модульное (Unit) тестирование?
  • Зачем оно нужно?
  • Как его провести?
  • Методы модульного тестирования
  • Разработка через тестирование (TDD)
  • Преимущества модульного тестирования
  • Недостатки модульного тестирования
  • Рекомендации по модульному тестированию

В моделях разработки SDLC, STLC, V Model модульное тестирование – это первый уровень тестирования, выполняемый перед интеграционным тестированием. Модульное тестирование – это метод тестирования WhiteBox, который обычно выполняется разработчиком. На деле же из-за нехватки времени или халатности разработчиков, иногда модульное тестирование приходится проводить QA инженерам.

Зачем нужно модульное тестирование?

Отсутствие модульного тестирования при написании кода значительно увеличивает уровень дефектов при дальнейшем (интеграционном, системном, и приемочном) тестировании. Качественное модульное тестирование на этапе разработки экономит время , а следовательно, в конечном итоге, и деньги.

Модульное тестирование
Unit Testing
Интеграционное тестирование
Integration Testing
Системное тестирование
System Testing
Приемочное тестирование
Acceptance Testing

  1. Модульные тесты позволяют исправить ошибки на ранних этапах разработки и снизить затраты.
  2. Это помогает разработчикам лучше понимать кодовую базу проекта и позволяет им быстрее и проще вносить изменения в продукт.
  3. Хорошие юнит-тесты служат проектной документацией.
  4. Модульные тесты помогают с миграцией кода. Просто переносите код и тесты в новый проект и изменяете код, пока тесты не запустятся снова.
  • Разработчик записывает в приложение единицу кода, чтобы протестировать ее. После: они комментируют и, наконец, удаляют тестовый код при развертывании приложения.
  • Разработчик может изолировать единицу кода для более качественного тестирования. Эта практика подразумевает копирование кода в собственную среду тестирования . Изоляция кода помогает выявить ненужные зависимости между тестируемым кодом и другими модулями или пространствами данных в продукте .
  • Кодер обычно использует UnitTest Framework для разработки автоматизированных тестовых случаев. Используя инфраструктуру автоматизации, разработчик задает критерии теста для проверки корректного выполнения кода, и в процессе выполнения тестовых случаев регистрирует неудачные. Многие фреймворки автоматически отмечают и сообщают, о неудачных тестах и могут остановить последующее тестирование, опираясь на серьезность сбоя.
  • Алгоритм модульного тестирования:
    • Создание тестовых случаев
    • Просмотр / переработка
    • Базовая линия
    • Выполнение тестовых случаев.

    Ниже перечислены методы покрытия кода:

    • Заявление покрытия
    • Охват решений
    • Охват филиала
    • Состояние покрытия
    • Покрытие конечного автомата

    Пример модульного тестирования: фиктивные объекты

    Модульное тестирование основывается на создании фиктивных объектов для тестирования фрагментов кода, которые еще не являются частью законченного приложения. Подставные объекты заполняют недостающие части программы.

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

    Разработка через тестирование (TDD)

    Модульное тестирование в TDD включает в себя широкое использование платформ тестирования. Каркас модульного тестирования используется для создания автоматизированных модульных тестов. Структуры модульного тестирования не являются уникальными для TDD, но они необходимы для него. Ниже некоторые преимущества TDD:

    • Тесты написаны перед кодом
    • Можно положиться на тестирование фреймворков
    • Все классы в приложениях протестированы
    • Быстрая и простая интеграция
    • Разработчики, желающие узнать, какие функциональные возможности предоставляет модуль и как его использовать, могут взглянуть на модульные тесты, чтобы получить общее представление об API модуля.
    • Модульное тестирование позволяет программисту выполнить рефакторинг кода на этапе регрессионного тестирования и убедиться, что модуль все еще работает правильно. Процедура заключается в написании контрольных примеров для всех функций и методов, чтобы в случае, если изменение вызвало ошибку, его можно было быстро идентифицировать и исправить.
    • Можем тестировать части проекта, не дожидаясь завершения других.
    • Не выявит всех ошибок. Невозможно оценить все пути выполнения даже в самых тривиальных программах.
    • Модульное тестирование по своей природе ориентировано на единицу кода. Следовательно, он не может отловить ошибки интеграции или ошибки системного уровня.

    Рекомендации по модульному тестированию

    • Модульные тесты должны быть независимыми. В случае каких-либо улучшений или изменений в требованиях, тестовые случаи не должны меняться.
    • Тестируйте только один модуль за раз.
    • Следуйте четким и последовательным соглашениям об именах для ваших модульных тестов
    • В случае изменения кода в каком-либо модуле убедитесь, что для модуля имеется соответствующий тестовый пример, и модуль проходит тестирование перед изменением реализации.
    • Пофиксите все выявленные баги перед переходом к следующему этапу, как минимум в модели разработки SDLC.
    • Примите подход «тест, как ваш код». Чем больше кода вы пишете без тестирования, тем больше сценариев вам придется проверять на наличие ошибок в дальнейшем.

    Статья подготовлена на основе материалов сайта guru99.com

    Unit-тестирование

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

    «IT-специалист с нуля» наш лучший курс для старта в IT

    Зачем проводится unit-тестирование

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

    Профессия / 8 месяцев
    IT-специалист с нуля

    Попробуйте 9 профессий за 2 месяца и выберите подходящую вам

    vsrat_7 1 (1)

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

    Unit-тесты

    • поиск и исправление ошибок на ранних стадиях разработки программного продукта и, следовательно, снижение затрат в дальнейшем;
    • лучшее понимание разработчиками базового кода проекта, более простая и быстрая корректировка продукта;
    • повторное использование кода, в том числе с переносом (вместе с тестами) в другие продукты;
    • использование юнит-тестов как проектной документации, по которой разработчики, не знакомые с кодом, могут понять принцип его работы.

    Преимущества unit-тестирования

    Применять модульное тестирование при разработке программных продуктов рекомендуется по следующим причинам:

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

    Недостатки unit-тестирования

    Несмотря на свои достоинства, модульное тестирование не является панацеей от всех болезней кода:

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

    Модульное и интеграционное тестирование

    Часто unit-тестирование путают с интеграционным, но это два разных по реализации и назначению уровня проверки программного обеспечения. Отличительные особенности модульного тестирования:

    • узкая специализация — проверке подвергаются отдельные модули, а не все приложение в целом;
    • простая реализация — тестирование модулей по отдельности (особенно при параллельной разработке) достаточно легкое в плане реализации, может проводиться без привлечения внешних ресурсов.

    Напротив, интеграционное тестирование отличается следующими особенностями:

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

    В реальной практике эти два уровня тестирования не противопоставляются, а дополняют друг друга. Проверка каждого модуля снижает количество багов, которые обязательно проявятся при интеграции компонентов. А интеграционное тестирование позволит оценить взаимодействие программных модулей друг с другом и ядром приложения.

    Курс для новичков «IT-специалист
    с нуля» – разберемся, какая профессия вам подходит, и поможем вам ее освоить

    Виды и методы модульного тестирования

    Виды

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

    Автоматизированное. Unit-тестирование заключается в использовании специально разработанных тестовых сред, которые проверяют работу модуля и выявляют в ней ошибки. Такой подход имеет следующие особенности:

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

    Методы

    «Черного ящика». В этом случае тестирование происходит по входным и выходным сигналам модуля без анализа структуры его кода. Чаще всего такой метод применяется, когда проверку выполняет разработчик, который не участвовал в создании компонента.

    «Белого ящика». Суть этого метода в том, что тестируются внутренняя структура модуля, его возможности, особенности поведения, реакция на входные сигналы и т.д. Иными словами, компонент изначально полностью прозрачен и понятен разработчику, который оценивает все внутренние и внешние аспекты его работы.

    Для понимания unit-тестирования рассмотрим подробнее, как оно происходит по методу «белого ящика». В этом случае оно состоит из трех этапов:

    • Анализ отдельного модуля. На этой стадии тестирования разработчик изучает внутреннюю структуру кода, функционал и поведение исследуемого компонента. Данный этап пройдет значительно быстрее, если программист сам создавал модуль или участвовал в его создании. Если нет — ему придется поднимать соответствующую документацию, консультироваться с создателем тестируемого фрагмента кода. Главная задача заключается в полном понимании того, как устроен и работает проверяемый программный компонент.
    • Создание кейс-теста. Это сценарий или модель, которые должны показать, как проверяемый модуль ведет себя в реальной обстановке. Кейс-тесты создают искусственную среду, максимально близкую к реальной, но без привлечения внешних ресурсов, которые обычно задействуются в работе программного обеспечения (веб-серверов, баз данных и т.д.).
    • Тестирование модуля. Проверяемый компонент, предварительно изолированный от ядра приложения и других модулей, запускается в кейс-тесте. При этом разработчик смотрит на то, как он реагирует на входные сигналы, как работает сам код, соответствует ли его структура выполняемым задачам, анализирует возможные ошибки и т.д.

    Часто к одному и тому же компоненту ПО разработчик применяет различные методики тестирования. Указанные методы «черного и белого ящиков» не исчерпывают всех методик и инструментов проверки. Зачастую разработчик создает под каждый проект уникальные способы тестирования, учитывающие особенности программного продукта.

    Разработка через тестирование

    Стандартна ситуация, когда разработчик сначала написал код, а затем создает под него тест и выполняет проверку. Но в программировании часто используется и обратный процесс: сначала разрабатывается тест, а модуль создается на его основе. Такой подход называется «разработка через тестирование». Суть его в том, чтобы с помощью заранее написанного теста определить требования к будущему программному компоненту. Цикл разработки через тестирование насчитывает несколько этапов:

    • Добавление теста. Оно происходит перед добавлением каждой новой функции в программу. Написанный тест не запускается по причине того, что проверяемый фрагмент кода еще не написан. Если тестирование сработало — значит, аналогичная или похожая функция в программе уже есть или тест написан некорректно. Сам тест тоже представляет собой программу, поэтому разработчик предварительно должен четко понять, какие результаты она должна показать в случае успешного тестирования.
    • Написание кода. Ориентируясь на то, как должна себя повести тест-программа в «идеальном» случае, разработчик пишет код самого модуля. Причем он не обязан быть сразу совершенным — все неточности будут отшлифованы в последующих циклах разработки. Главное, что требуется от кода, — это прохождение теста. Как только разрабатываемый фрагмент написан, он прогоняется через тест-программу и анализируется.
    • Рефакторинг. Убедившись, что написанный модуль успешно проходит тест, разработчик проверяет его на дублирование, неточности, мусорный код и т.д. Задача на этом этапе — максимально очистить фрагмент, сделать его более прозрачным, простым и понятным.

    Разработка через тестирование не ограничивается одним циклом: они повторяются каждый раз при добавлении в приложение новых функций, процессов или других объектов. Если в очередной итерации ранее проходивший тестирование код вдруг выдал ошибку, разработчик всегда может откатить внесенные изменения, которые ее вызвали.

    Этот метод разработки имеет свои преимущества:

    • Код становится более простым и понятным, так как пишется под конкретные требования, заданные в тесте.
    • Сокращается время разработки, в том числе за счет более частого использования отката модуля к работающей версии, чем отладки неработающей.
    • Дизайн программы становится более удобным для пользователей, так как продумывается заранее, до написания кода, а не подгоняется под него.
    • Снижается количество багов, так как разработчик изначально знает, что хочет получить от своего кода, а не использует метод проб и ошибок.
    • Заранее написанный тест можно использовать в дальнейшем в качестве проектной документации к программному продукту.

    Рекомендации к unit-тестам

    Чтобы модульное тестирование было максимально эффективным, тесты должны:

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

    Когда не стоит проводить unit-тестирование

    Модульное тестирование — не универсальный инструмент проверки программного продукта. В некоторых ситуациях оно лишь отнимет время и силы, не показав значимого результата, например:

    • при тестировании сложных и разветвленных алгоритмов, таких как красно-черное дерево, придется разработать большое число тестов, что существенно усложнит и замедлит проверку;
    • отсутствии четких результатов — например, в математическом моделировании природных процессов, настолько сложных, что их «выход» невозможно спрогнозировать, а можно только описать в виде интервалов вероятных значений;
    • тестировании кода, взаимодействующего с системой, — например, модуля, связанного с портами, таймерами и другими «нестабильными» компонентами, от которых его сложно изолировать;
    • проверке всего приложения — модульное тестирование не покажет ошибки интеграции, баги ядра и другие аспекты, не относящиеся непосредственно к конкретному модулю;
    • недостаточной квалификации самого разработчика и низкой культуре программирования, так как модульное тестирование работает только при строгом соблюдении технологии, постоянном отслеживании всех вносимых в модуль изменениях.

    Unit-тестирование окажется бесполезным и при проверке максимально простого кода. Точнее, оно сработает и покажет правильный результат, но сил на написание теста уйдет больше, чем на «ручной» анализ модуля.

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

    IT-специалист с нуля

    Наш лучший курс для старта в IT. За 2 месяца вы пробуете себя в девяти разных профессиях: мобильной и веб-разработке, тестировании, аналитике и даже Data Science — выберите подходящую и сразу освойте ее.

    картинка (75)

    Статьи по теме:

    Юнит-тестирование для чайников

    Даже если вы никогда в жизни не думали, что занимаетесь тестированием, вы это делаете. Вы собираете свое приложение, нажимаете кнопку и проверяете, соответствует ли полученный результат вашим ожиданиям. Достаточно часто в приложении можно встретить формочки с кнопкой “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 группы.

    Рассмотрим сначала экстремальные случаи: простой код без зависимостей и сложный код с большим количеством зависимостей.

    1. Простой код без зависимостей. Скорее всего здесь и так все ясно. Его можно не тестировать.
    2. Сложный код с большим количеством зависимостей. Хм, если у вас есть такой код, тут пахнет God Object’ом и сильной связностью. Скорее всего, неплохо будет провести рефакторинг. Мы не станем покрывать этот код юнит-тестами, потому что перепишем его, а значит, у нас изменятся сигнатуры методов и появятся новые классы. Так зачем писать тесты, которые придется выбросить? Хочу оговориться, что для проведения такого рода рефакторинга нам все же нужно тестирование, но лучше воспользоваться более высокоуровневыми приемочными тестами. Мы рассмотрим этот случай отдельно.
    1. Cложный код без зависимостей. Это некие алгоритмы или бизнес-логика. Отлично, это важные части системы, тестируем их.
    2. Не очень сложный код с зависимостями. Этот код связывает между собой разные компоненты. Тесты важны, чтобы уточнить, как именно должно происходить взаимодействие. Причина потери 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 и все остальное – это для продакшна. А в тестах допустимо все. Это не верно. Тесты – такой-же код. Разница только в том, что у тестов другая цель – обеспечить качество вашего приложения. Все принципы, применямые в разработке продакшн-кода могут и должны применяться при написании тестов.
    Есть всего три причины, почему тест перестал проходить:

    1. Ошибка в продакшн-коде: это баг, его нужно завести в баг-трекере и починить.
    2. Баг в тесте: видимо, продакшн-код изменился, а тест написан с ошибкой (например, тестирует слишком много или не то, что было нужно). Возможно, что раньше он проходил ошибочно. Разберитесь и почините тест.
    3. Смена требований. Если требования изменились слишком сильно – тест должен упасть. Это правильно и нормально. Вам нужно разобраться с новыми требованиями и исправить тест. Или удалить, если он больше не актуален.

    Уделяйте внимание поддержке ваших тестов, чините их вовремя, удаляйте дубликаты, выделяйте базовые классы и развивайте API тестов. Можно завести шаблонные базовые тестовые классы, которые обязывают реализовать набор тестов (например CRUD). Если делать это регулярно, то вскоре это не будет занимать много времени.

    Как «измерить» прогресс

    Для измерения успешности внедрения юнит-тестов в вашем проекте следует использовать две метрики:

    1. Количество багов в новых релизах (в т.ч. и регрессии)
    2. Покрытие кода

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

    • NCover
    • dotTrace
    • встроенный в студию Test Coverage

    Test First?

    Я умышленно не касался этой темы до самого конца. С моей точки зрения Test First – хорошая практика, обладающая рядом неоспоримых преимуществ. Однако, по тем или иным причинам, иногда я отступаю от этого правила и пишу тесты после того, как готов код.

    На мой взгляд, «как писать тесты» гораздо важнее, чем «когда это делать». Делайте, как вам удобно, но не забывайте: если вы начинаете с тестов, то получаете архитектуру «в придачу». Если вы сначала пишете код, вам возможно, придется его менять, чтобы сделать тестируемым.

    Почитать на тему

    Отличную подборку ссылок и книг по теме можно найти в этой статье на Хабре. Особенно рекомендую книгу The Art of Unit Testing. Я читал первое издание. Оказывается, вышло уже и второе.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *