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

Из Википедии, бесплатной энциклопедии

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

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

Преимущества[править | править код]

Цель модульного тестирования — изолировать отдельные части программы и показать, что по отдельности эти части работоспособны.

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

Поощрение изменений[править | править код]

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

Упрощение интеграции[править | править код]

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

Документирование кода[править | править код]

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

Отделение интерфейса от реализации[править | править код]

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

Когда модульное тестирование не работает[править | править код]

Сложный код[править | править код]

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

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

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

Результат известен лишь приблизительно[править | править код]

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

Код, взаимодействующий с системой[править | править код]

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

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

Многопоточность[править | править код]

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

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

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

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

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

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

Проблемы с объектами-заглушками[править | править код]

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

Разработка встраиваемого ПО[править | править код]

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

Приложения модульного тестирования[править | править код]

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

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

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

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

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

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

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

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

Тесты не должны физически зависеть друг от друга. Вышеуказанная пара «создать пустую картинку» и «загрузить из 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

Примечания[править | править код]

См. также[править | править код]

Литература[править | править код]

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

Ссылки[править | править код]

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