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

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

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

Преимущества

Цель модульного тестирования — изолировать отдельные части программы и показать, что по отдельности эти части работоспособны. Также, чтобы проверить, что каждая единица программного кода работает должным образом. Данный вид тестирование выполняется разработчиками на этапе кодирования приложения. Модульные тесты изолируют часть кода и проверяют его работоспособность.[1]

Этот тип тестирования обычно выполняется программистами.

Поощрение изменений

Модульное тестирование позже позволяет программистам проводить рефакторинг, будучи уверенными, что модуль по-прежнему работает корректно (регрессионное тестирование). Это поощряет программистов к изменениям кода, поскольку достаточно легко проверить, что код работает и после изменений.

Упрощение интеграции

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

Документирование кода

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

Отделение интерфейса от реализации

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

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

Сложный код

Тестирование программного обеспечения — комбинаторная задача. Например, каждое возможное значение булевой переменной потребует двух тестов: один на вариант TRUE, другой — на вариант FALSE. В результате на каждую строку исходного кода потребуется 3−5 строк тестового кода.

Алгоритмы вроде Marching cubes или красно-чёрного дерева имеют разветвлённое дерево решений, и чтобы проверить все варианты, нужны огромные наборы тестов: в одной из реализаций красно-чёрного дерева с GitHub на проверку вставки сделано двенадцать тестов[2]. В другой — автоматически строят 10! = 3,6 млн перестановок и испытывают их все[3].

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

Результат известен лишь приблизительно

Например, в математическом моделировании. Бизнес-приложения зачастую работают с конечными и счётными множествами, научные — с континуальными.[4] Поэтому сложно подобрать тесты для каждой из ветвей программы, сложно сказать, верен ли результат, выдерживается ли точность, и т. д. А во многих случаях качество моделирования определяется «на глаз», и последний результат записывается как «опорный». Если найдено расхождение, новый результат проверяют вручную и выясняют, какой качественнее: старый или новый.

Код, взаимодействующий с системой

Код, взаимодействующий с портами, таймерами, пользователем и прочими «нестабильными» частями системы, крайне сложно проверить в изолированном окружении.

Но это не значит, что модульное тестирование здесь полностью непригодно: оно вынуждает программиста перейти от файлов и портов, например, на абстрактные потоки. Это делает код более общим (например, без проблем можно перейти с файлов на сетевые сокеты), более тестируемым (можно смоделировать для высокоуровневого кода ситуацию «пропала связь», написав поток, который, выдав N байт, смоделирует аварию; проверить под Windows часть функций преобразования путей Unix), ограничивает те части, которые не подлежат модульному тестированию.

Многопоточность

Это в принципе нестабильная часть системы[5], и если при какой-то редкой последовательности событий случается взаимная блокировка, очень сложно сделать даже код, повторяющий её с некоторой вероятностью, не говоря уже о надёжном повторении. К тому же модульные тесты обычно просты, а тесты для многопоточных систем, наоборот, должны быть достаточно велики.

Обычно модульные тесты многократно повторяют тестовый сценарий, рассчитывая, что ошибка рано или поздно выплывет[5].

Ошибки интеграции и производительности

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

При общей низкой культуре программирования

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

За исключением простейших случаев, тестируемый объект должен взаимодействовать с другими объектами. Этих «товарищей по взаимодействию» — объекты-заглушки — делают предельно простыми: либо крайне упрощёнными (память вместо базы данных), либо рассчитанными на конкретный тест и механически повторяющими сессию обмена. Проблемы могут возникать при смене протокола обмена, в таком случае объекты-заглушки должны отвечать новым требованиям протокола.[6]

Разработка встраиваемого ПО

Легко убедиться, что модуль работает на машине разработчика. Сложнее — что на целевой машине, зачастую сильно ограниченной[7].

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

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

Экстремальное программирование предполагает как один из постулатов использование инструментов автоматического модульного тестирования. Этот инструментарий может быть создан либо третьей стороной (например, Boost.Test), либо группой разработчиков данного приложения.

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

Программные средства тестирования

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

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

Сложность написания модульных тестов зависит от самой организации кода. Сильное зацепление или большая зона ответственности отдельных сущностей (классы для объектно-ориентированных языков) могут усложнить тестирование. Для объектов осуществляющих связь с внешним миром (сетевое взаимодействие, файловый ввод-вывод и т. д.) следует создавать заглушки. В терминологии выделяют более «продвинутые» заглушки — Mock-объекты, которые несут в себе логику. Также упростить тестирование может выделение как можно большей части логики в чистые функции. Они никак не взаимодействуют с внешним миром и их результат зависит только от входных параметров.

Код тестов принято выделять в отдельные каталоги. Желательно, чтобы добавление новых тестов в проекте не было сложной задачей и была возможность запускать все тесты. Некоторые системы контроля версий, например git, поддерживают хуки (англ. hook), с помощью которых можно настроить запуск всех тестов перед фиксированием изменений. При ошибке в хотя бы одном из тестов, изменения зафиксированы не будут. Также можно применять системы непрерывной интеграции.

Простейшие требования к модульным тестам

Иногда говорят, что модульный тест должен содержать всего одно проверяемое условие. Это неверно, модульный тест должен проверять одну концепцию одной сессией обмена — например, работу конструктора: пустая картинка должна иметь размеры 0×0 и не выделять памяти. А загрузка из PNG — другая концепция и проверяется другим тестом. Но бессмысленно загружать в картинку сначала одни, потом другие данные, если мы не проверяем корректность работы в таком режиме.

Бывают случаи, когда от одной концепции и одной сессии обмена отходят:

  • «Простейшую функциональность сложной функции» можно разбить на меньшие концепции — корректное выделение памяти, правильный расчёт и т.д., но не всегда представляется возможным разбить код на меньшие части, чтобы проверить это по отдельности. При переводе на модульное тестирование старого кода, проверенного практикой, также поначалу можно обойтись «простейшей функциональностью».
  • Несколько однострочников, проверяющих одну концепцию, могут объединить в один тест:
EXPECT_TRUE (startsWith(L"", L""));
EXPECT_FALSE(startsWith(L"", L"1"));
...

Тесты не должны физически зависеть друг от друга. Упомянутая пара «создать пустую картинку» и «загрузить из PNG» должны оперировать каждый своими локальными переменными — это позволяет удалить тест, если пропадает тестируемая концепция, или закомментировать все тесты, кроме неудачного. Системы модульного тестирования могут запускать тесты в произвольном порядке и даже параллельно.

Но тесты могут логически зависеть друг от друга. Например: если проверена концепция «объект корректно создаётся», на неё можно полагаться и больше не проверять.

Тесты могут физически зависеть от общих неизменных наборов данных. Например, разные преобразования картинки (поворот, отражение) могут начинать с одного PNG фотографического качества — на таких картинках достаточно проверить несколько избранных пикселей, и будет достаточно высокая вероятность, что поворот действительно выполняется.

Если выяснилось, что протестированный код действует неверно, но тесты проходят и верны (или не проходит тест, проверяющий совсем другую концепцию — проверяется верность расчёта, а ошибка в выделении памяти), принято действовать так:

  • Изолировать ошибку — придумать короткий повторяемый пример, когда код работает неверно.
  • Написать новый тест, который не проходит.
  • Исправить ошибку.
  • Убедиться, что все тесты проходят — и старые, и новый.

Функциональное и нефункциональное тестирование

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

Инструментарий

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

Поддержка на уровне языка

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

Пример таких языков:

Пример кода на языке D

class ABC
{
    this() { val = 2; }

    private int val;
    public func() { val *= 2; }
}

unittest
{
   ABC a;
   a.func();

   assert( a.val > 0 && a.val < 555 ); // можно обратиться к приватной переменной внутри модуля
}

Пример кода на языке Pyret

fun my-max(lst):
  doc: ```Находит максимальное число в непустом списке.
       Если список пуст, вызывает исключение.```
  cases (List) lst:
    | empty => raise("List is empty")
    | link(fst, rst) =>
      cases (List) rst:
        | empty => fst
        | link(snd, rst-of-rst) => num-max(fst, my-max(rst))
      end
  end
where:
  my-max(empty) raises "List is empty"
  my-max([list: 1]) is 1
  my-max([list: -100]) is -100
  my-max([list: 1, -100]) is 1
  my-max([list: 1, 2, 3, 4, 3, 2, 1]) is 4
end

Примечания

  1. СЛЕГТИНА В.А. МЕТОДИЧЕСКИЕ РЕКОМЕНДАЦИИ ПРОВЕДЕНИЯ МОДУЛЬНОГО ТЕСТИРОВАНИЯ С ИСПОЛЬЗОВАНИЕМ БИБЛИОТЕКИ JUNIT // ФУНДАМЕНТАЛЬНЫЕ И ПРИКЛАДНЫЕ НАУЧНЫЕ ИССЛЕДОВАНИЯ. — 2023. — С. 199-204.
  2. GitHub - xieqing/red-black-tree: A Red-black Tree Implementation In C. Дата обращения: 14 апреля 2022. Архивировано 14 апреля 2022 года.
  3. http://orion.lcg.ufrj.br/java/bigjava/ch17/worked_example_2/RedBlackTreeTester.java
  4. Почему юнит-тесты не работают в научных приложениях / Хабрахабр. Дата обращения: 9 июня 2014. Архивировано 14 июля 2014 года.
  5. 1 2 Источник. Дата обращения: 10 мая 2023. Архивировано 21 марта 2023 года.
  6. Проблема дублирования и устаревания знания в mock-объектах или Интеграционные тесты — это хорошо / Хабрахабр. Дата обращения: 19 января 2016. Архивировано 19 января 2016 года.
  7. Marek Kucharski Making Unit Testing Practical for Embedded Development Архивная копия от 25 мая 2022 на Wayback Machine
  8. ГУЛЯЕВ Н.А. СИСТЕМАТИЗАЦИЯ АКТУАЛЬНЫХ ПРОГРАММНЫХ СРЕДСТВ ТЕСТИРОВАНИЯ ПРОИЗВОДИТЕЛЬНОСТИ МОДУЛЬНЫХ ЭВМ // СОВЕРШЕНСТВОВАНИЕ НАУКИ И ОБРАЗОВАНИЯ В ОБЛАСТИ ЕСТЕСТВЕННЫХ И ТЕХНИЧЕСКИХ ИССЛЕДОВАНИЙ. — 2023. — С. 195-197.

См. также

Литература

  • Ошероув, Р. Искусство автономного тестирования с примерами на C# = The Art Of Unit Testing Second Edition With Examples In C#. – ДМК Пресс, 2016. – ISBN 978-5-97060-415-1.

Ссылки

Сайты и ресурсы

Статьи