Введение

Проверка поведения изолированного исходного кода

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

История

Юнит-тестирование, как принцип тестирования отдельных, меньших частей больших программных систем, восходит к ранним дням разработки программного обеспечения. В июне 1956 года Х. Д. Бенингтон представил на симпозиуме ВМС США по передовым методам программирования для цифровых компьютеров проект SAGE и его подход, основанный на спецификации, где за фазой кодирования следовало "параметрическое тестирование" для проверки компонентных подпрограмм на соответствие их спецификации, а затем "тестирование сборки" для собранных частей. В 1964 году аналогичный подход описан для программного обеспечения проекта Mercury, где отдельные модули, разработанные разными группами, проходили "юнит-тесты" перед интеграцией. В 1969 году методологии тестирования стали более структурированными, с юнит-тестами, компонентными тестами и интеграционными тестами, предназначенными для проверки отдельных частей, написанных независимо, и их последовательной сборки в более крупные блоки. Некоторые общедоступные стандарты, принятые в конце 60-х годов, такие как MIL STD 483 и MIL STD 490, способствовали широкому распространению юнит-тестирования в крупных проектах. В те времена юнит-тестирование было интерактивным, с использованием либо закодированных тестов, либо инструментов захвата и воспроизведения тестирования. В 1989 году Кент Бек описал тестовый фреймворк для Smalltalk (позже названный SUnit) в книге "Простое тестирование Smalltalk: с использованием шаблонов". В 1997 году Кент Бек и Эрих Гамма разработали и выпустили JUnit, юнит-тестовый фреймворк, который стал популярен среди разработчиков Java. Google внедрил автоматизированное тестирование примерно в 2005–2006 годах.

Единицы

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

Исполнение

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

Критерии испытаний

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

Параметризированное испытание

Параметризованный тест — это тест, который принимает набор значений, используемых для запуска теста с различными входными данными. Фреймворк тестирования, поддерживающий параметризованные тесты, предоставляет способ задания наборов параметров и запуска теста для каждого из них. Использование параметризованных тестов позволяет сократить дублирование тестового кода. Параметризованные тесты поддерживаются TestNG, JUnit, XUnit и NUnit, а также различными JavaScript-фреймворками для тестирования. Параметры для модульных тестов могут быть заданы вручную или, в некоторых случаях, автоматически сгенерированы самим фреймворком. В последние годы появилась поддержка написания более сложных (модульных) тестов с использованием концепции теорий — тестовых случаев, выполняющих одни и те же шаги, но использующих тестовые данные, генерируемые во время выполнения, в отличие от обычных параметризованных тестов, которые используют предопределенные наборы входных данных с одинаковыми шагами выполнения.

Гибкий

Иногда в гибкой разработке программного обеспечения модульное тестирование выполняется для каждой пользовательской истории и проводится во второй половине спринта после сбора требований и завершения разработки. Обычно разработчики или другие члены команды разработки, такие как консультанты, пишут пошаговые "тест-скрипты" для выполнения разработчиками в инструменте. Тест-скрипты обычно создаются для подтверждения корректной и технической работы конкретных разработанных функций в инструменте, в отличие от полноценных бизнес-процессов, с которыми взаимодействует конечный пользователь, что обычно выполняется во время приемочного тестирования. Если тест-скрипт может быть полностью выполнен от начала до конца без ошибок, модульный тест считается "успешно пройденным", в противном случае фиксируются ошибки, и пользовательская история возвращается в разработку в состояние "в работе". Пользовательские истории, успешно прошедшие модульное тестирование, переходят к финальным этапам спринта: проверке кода, взаимному рецензированию, а затем – демонстрационной сессии ("showback"), на которой разработанный инструмент представляется заинтересованным сторонам.

Разработка на основе испытаний

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

Стоимость

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

Более частые выпуски

Юнит-тестирование позволяет выпускать программное обеспечение чаще. Тестируя отдельные компоненты изолированно, разработчики могут быстро выявлять и устранять проблемы, что ускоряет итерации и циклы выпуска.

Разрешает рефакторинг кода

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

Определяет изменения, которые могут нарушить проектный договор

Юнит-тесты выявляют изменения, которые могут нарушить контракт проектирования.

Уменьшить неопределенность

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

Документация поведения системы

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

Ограничения и недостатки

Тестирование не выявит каждую ошибку в программе, поскольку оно не может оценить каждый путь выполнения, кроме самых тривиальных программ. Эта проблема является обобщением проблемы останова, которая неразрешима. То же самое справедливо и для модульного тестирования. Кроме того, модульное тестирование по определению проверяет только функциональность самих модулей. Следовательно, оно не обнаружит ошибки интеграции или более масштабные системные ошибки (например, функции, выполняемые между несколькими модулями, или области тестирования, не связанные с функциональностью, такие как производительность). Модульное тестирование должно проводиться в сочетании с другими видами тестирования программного обеспечения, поскольку они могут лишь показать наличие или отсутствие конкретных ошибок, но не могут доказать полное отсутствие ошибок. Чтобы гарантировать правильное поведение для каждого пути выполнения и каждого возможного ввода, а также обеспечить отсутствие ошибок, необходимы другие методы, а именно применение формальных методов для доказательства отсутствия нежелательного поведения в программном компоненте. Сложная иерархия модульных тестов не эквивалентна интеграционному тестированию. Интеграция с внешними модулями должна быть включена в интеграционные тесты, но не в модульные. Интеграционное тестирование по-прежнему часто сильно зависит от ручного тестирования; высокоуровневое или глобальное тестирование может быть сложно автоматизировать, поэтому ручное тестирование часто кажется более быстрым и дешевым. Тестирование программного обеспечения – это комбинаторная задача. Например, каждое логическое условие требует как минимум двух тестов: один с результатом "истина" и один с результатом "ложь". В результате, на каждую строку кода программистам часто требуется от 3 до 5 строк тестового кода. Это, очевидно, требует времени, и инвестиции в это могут не оправдать себя. Существуют проблемы, которые сложно тестировать вообще, например, недетерминированные или многопоточные. Кроме того, код для модульного теста с такой же вероятностью может содержать ошибки, как и код, который он тестирует. Фред Брукс в книге "Мифический человек-месяц" цитирует: "Никогда не выходите в море с двумя хронометрами; берите один или три". Смысл в том, что если два хронометра показывают разные результаты, как узнать, какой из них верен?

Трудности в создании реалистичных и полезных тестов

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

Требует дисциплины на протяжении всего процесса разработки

Для получения ожидаемой пользы от модульного тестирования требуется строгая дисциплина на протяжении всего процесса разработки программного обеспечения.

Требуется контроль версий

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

Требует регулярных обзоров

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

Ограничения для программного обеспечения встроенной системы

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

Ограничения при тестировании интеграции с внешними системами

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

В качестве исполняемых спецификаций

Использование модульных тестов в качестве технического задания имеет одно существенное преимущество перед другими методами проектирования: сам документ проектирования (то есть модульные тесты) может быть использован для проверки реализации. Тесты не пройдут, пока разработчик не реализует решение в соответствии с заданным проектом. Модульное тестирование уступает в наглядности диаграммным спецификациям, таким как UML-диаграммы, но их можно сгенерировать из модульных тестов с помощью автоматизированных инструментов. Большинство современных языков программирования имеют бесплатные инструменты, обычно доступные в виде расширений для IDE. Бесплатные инструменты, такие как основанные на фреймворке xUnit, передают задачу графического представления информации для удобства восприятия человеку другой системе.

Экстремальное программирование

Юнит-тестирование является краеугольным камнем экстремального программирования, которое опирается на автоматизированную систему юнит-тестирования. Эта автоматизированная структура юнит-тестирования может быть реализована с использованием сторонних средств, например, xUnit, или создана внутри команды разработчиков. Экстремальное программирование использует создание юнит-тестов для разработки через тестирование (TDD). Разработчик пишет юнит-тест, который выявляет либо требование к программному обеспечению, либо дефект. Этот тест изначально не проходит, потому что требование еще не реализовано, либо потому что он намеренно выявляет дефект в существующем коде. Затем разработчик пишет минимальный код, необходимый для прохождения этого теста, а также других тестов. Большая часть кода в системе подвергается юнит-тестированию, но не обязательно все возможные пути выполнения кода. Экстремальное программирование предписывает стратегию "тестировать все, что потенциально может сломаться", в отличие от традиционного подхода "тестировать каждый путь выполнения". Это приводит к тому, что разработчики создают меньше тестов, чем при использовании классических методов, но это скорее констатация факта, чем проблема, поскольку классические методы редко выполнялись достаточно методично, чтобы обеспечить тщательное тестирование всех путей выполнения. Экстремальное программирование просто признает, что тестирование редко бывает исчерпывающим (поскольку оно часто слишком дорого и трудоемко, чтобы быть экономически оправданным) и предоставляет рекомендации по эффективному распределению ограниченных ресурсов. Важно отметить, что тестовый код рассматривается как полноценный артефакт проекта и поддерживается с тем же качеством, что и код реализации, при этом исключается любое дублирование. Разработчики публикуют код юнит-тестов в репозиторий кода одновременно с кодом, который он тестирует. Тщательное юнит-тестирование в экстремальном программировании позволяет получить преимущества, такие как более простая и уверенная разработка и рефакторинг кода, упрощенная интеграция кода, точная документация и более модульная архитектура. Эти юнит-тесты также постоянно выполняются в качестве регрессионного тестирования. Юнит-тестирование также критически важно для концепции Emergent Design (эмерджентного проектирования). Поскольку эмерджентный дизайн сильно зависит от рефакторинга, юнит-тесты являются его неотъемлемой частью.

Автоматизированные системы тестирования

Автоматизированный тестовый фреймворк предоставляет средства для автоматизации выполнения тестов и может ускорить разработку и запуск тестов. Фреймворки были разработаны для широкого спектра языков программирования. Как правило, фреймворки являются сторонними и не поставляются вместе с компилятором или интегрированной средой разработки (IDE). Тесты можно писать без использования фреймворка, проверяя тестируемый код с помощью утверждений, обработки исключений и других механизмов управления потоком выполнения для верификации поведения и сообщения об ошибках. Некоторые считают, что тестирование без фреймворка полезно, поскольку внедрение фреймворка может быть затруднительным; лучше иметь хоть какие-то тесты, чем не иметь их вовсе, но после внедрения фреймворка добавление новых тестов может стать проще. В некоторых фреймворках отсутствуют продвинутые функции тестирования, которые необходимо реализовывать вручную.