Стратегии, методологии и подходы в интеграционном тестировании
Программная инженерия задает различные стратегии интеграционного тестирования:
- Подход Большого взрыва.
- Инкрементальный подход:
- Нисходящий подход (сверху вниз)
- Подход «снизу вверх»
- Сэндвич – комбинация «сверху вниз» и «снизу вверх»
Ниже приведены различные стратегии, способы их выполнения и их ограничения, а также преимущества.
Подход Большого взрыва
Здесь все компоненты собираются вместе, а затем тестируются.
Преимущества:
- Удобно для небольших систем.
Недостатки:
- Сложно локализовать баги.
- Учитывая огромное количество интерфейсов, некоторые из них при тестировании можно запросто пропустить.
- Недостаток времени для группы тестирования, т.к тестирование интеграции может начаться только после того, как все модули спроектированы.
- Поскольку все модули тестируются одновременно, критические модули высокого риска не изолируются и тестируются в приоритетном порядке. Периферийные модули, которые имеют дело с пользовательскими интерфейсами, также не изолированы и не проверены на приоритет.
Инкрементальный подход
В данном подходе тестирование выполняется путем объединения двух или более логически связанных модулей. Затем добавляются другие связанные модули и проверяются на правильность функционирования. Процесс продолжается до тех пор, пока все модули не будут соединены и успешно протестированы.
Поэтапный подход, в свою очередь, осуществляется двумя разными методами:
- Снизу вверх
- Сверху вниз
Заглушка и драйвер
Инкрементальный подход осуществляется с помощью фиктивных программ, называемых заглушками и драйверами. Заглушки и драйверы не реализуют всю логику программного модуля, а только моделируют обмен данными с вызывающим модулем.
Заглушка: вызывается тестируемым модулем.
Драйвер: вызывает модуль для тестирования.
Интеграция «снизу вверх»
В восходящей стратегии каждый модуль на более низких уровнях тестируется с модулями более высоких уровней, пока не будут протестированы все модули. Требуется помощь драйверов для тестирования
Преимущества:
- Проще локализовать ошибки.
- Не тратится время на ожидание разработки всех модулей, в отличие от подхода Большого взрыва.
Недостатки:
- Критические модули (на верхнем уровне архитектуры программного обеспечения), которые контролируют поток приложения, тестируются последними и могут быть подвержены дефектам.
- Не возможно реализовать ранний прототип
Интеграция «сверху вниз»
При подходе «сверху вниз» тестирование, что логично, выполняется сверху вниз, следуя потоку управления программной системы. Используются заглушки для тестирования.
Преимущества:
- Проще локализовать баги.
- Возможность получить ранний прототип.
- Критические Модули тестируются на приоритет; основные недостатки дизайна могут быть найдены и исправлены в первую очередь.
Недостатки:
- Нужно много пней.
- Модули на более низком уровне тестируются неадекватно
Сэндвич (гибридная интеграция)
Эта стратегия представляет собой комбинацию подходов «сверху вниз» и «снизу вверх». Здесь верхнеуровневые модули тестируются с нижнеуровневыми, а нижнеуровневые модули интегрируются с верхнеуровневыми, соответственно, и тестируются. Эта стратегия использует и заглушки, и драйверы.
Как сделать интеграционное тестирование?
Алгоритм интеграционного тестирования:
- Подготовка план интеграционных тестов
- Разработка тестовых сценариев.
- Выполнение тестовых сценариев и фиксирование багов.
- Отслеживание и повторное тестирование дефектов.
- Повторять шаги 3 и 4 до успешного завершения интеграции.
Атрибуты Интеграционного тестирования
Включает в себя следующие атрибуты:
- Методы / Подходы к тестированию (об этом говорили выше).
- Области применения и Тестирование интеграции.
- Роли и обязанности.
- Предварительные условия для Интеграционного тестирования.
- Тестовая среда.
- Планы по снижению рисков и автоматизации.
Критерии старта и окончания интеграционного тестирования
Критерии входа и выхода на этап Интеграционного тестирования, независимо от модели разработки программного обеспечения
Критерии старта:
- Модули и модульные компоненты
- Все ошибки с высоким приоритетом исправлены и закрыты
- Все модули должны быть заполнены и успешно интегрированы.
- Наличие плана Интеграционного тестирования, тестовый набор, сценарии, которые должны быть задокументированы.
- Наличие необходимой тестовой среды
Критерии окончания:
- Успешное тестирование интегрированного приложения.
- Выполненные тестовые случаи задокументированы
- Все ошибки с высоким приоритетом исправлены и закрыты
- Технические документы должны быть представлены после выпуска Примечания.
Лучшие практики / рекомендации по интеграционному тестированию
- Сначала определите интеграционную тестовую стратегию, которая не будет противоречить вашим принципам разработки, а затем подготовьте тестовые сценарии и, соответственно, протестируйте данные.
- Изучите архитектуру приложения и определите критические модули. Не забудьте проверить их на приоритет.
- Получите проекты интерфейсов от команды разработки и создайте контрольные примеры для проверки всех интерфейсов в деталях. Интерфейс к базе данных / внешнему оборудованию / программному обеспечению должен быть детально протестирован.
- После тестовых случаев именно тестовые данные играют решающую роль.
- Всегда имейте подготовленные данные перед выполнением. Не выбирайте тестовые данные во время выполнения тестовых случаев.
Статья подготовлена на основе материалов сайта guru99.com
Общая картина модульного тестирования
Время прочтения
10 мин
Просмотры 29K
Это не руководство, какие символы нужно ввести в редакторе кода, чтобы получились модульные тесты. Это — пища для ума, которую необходимо употребить до того, как предпринимать упомянутые действия.
Тема модульного тестирования не так проста, как может показаться. Многие из нас, разработчиков, приходят в модульное тестирование под давлением клиентов, сотрудников, коллег, своих кумиров и так далее. Мы быстро понимаем его ценность, и, закончив технические приготовления, забываем об общей картине, если вообще когда-либо её понимали. В этой статье я вкратце расскажу о том, чем является и чем не является модульное тестирование как в целом, так и в PHP, а заодно опишу, какое место занимает модульное тестирование в сфере QA.
Что такое тестирование?
Прежде чем углубляться в модульные тесты, нужно изучить теорию самого тестирования, чтобы не делать ошибок вроде той, что совершили авторы одного из самых популярных PHP-фреймворков: на своём сайте они показали интеграционные тесты и назвали их модульными. Нет, Laravel, это не модульные тесты. Хотя это не мешает мне всё ещё любить этот фреймворк.
Тестирование ПО определяется как «расследование, проведённое с целью предоставления заинтересованным сторонам информации о качестве продукта». Этому противопоставляется «тестирование ПО — это пустая трата бюджета проекта разработчиками, которые не делают ничего важного, а затем просят ещё времени и денег, потому что «ничего» может быть весьма дорогим». Тут ничего нового.
Вот моя краткая история становления тестирования:
- 1822 — Разностная машина (Difference engine) (Чарльз Бэббидж).
- 1843 — Аналитическая машина (Analytical engine) (Ада Лавлейс).
- 1878 — Эдисон вводит термин «баг».
- 1957 — Тестирование и отладка программ (Чарльз Бэйкер).
- 1958 — Первая команда тестирования ПО (Джеральд Вайнберг).
- 1968 — Кризис ПО (Фридрих Бауэр).
- 1970-е — Модель «водопад», реляционная модель, декомпозиция, критический анализ (Walkthrough), проектирование и инспектирование кода, качество и метрики, шаблоны проектирования.
- 1980-е — CRUD-анализ, архитектура системы, автотестирование, V-модель, надёжность, стоимость качества, способы использования, шаблоны ООП-проектирования.
- 1990-е — Scrum, usability-тестирование, MoSCoW, эвристическое тестирование, автоматизация ПО и тестирования.
Если вы относитесь к поколению миллениалов, как я, то, возможно, будете поражены, что команды тестировщиков существовали ЗАДОЛГО до вашего рождения. Остановитесь на минутку, вдохните, выдохните, успокойтесь.
История показывает, как в течение времени менялся тип тестирования, которое считалось «достаточно хорошим» для заинтересованных сторон. Примерные фазы, на что ориентировались при тестировании:
- … — 1956 отладка
- 1957 — 1978 демонстрация
- 1979 — 1982 разрушение (destruction)
- 1983 — 1987 оценка
- 1988 — … предотвращение
Следовательно, модульное тестирование необходимо для предотвращения несоответствий между проектом и реализацией.
Чем на самом деле является тестирование?
Есть разные классификации тестирования ПО. Чтобы лучше понимать место модульного тестирования, упомяну лишь о наиболее широкораспространённых подходах.
Тесты бывают: статические и динамические, «ящичные» (белый ящик, чёрный ящик, серый ящик), уровни и типы. В рамках каждого подхода используются разные критерии классификации.
Статическое и динамическое тестирование
Статическое тестирование проводится без исполнения кода. Сюда относится корректура, проверка, ревизия кода (при наблюдении за работой другого / парном программировании), критический анализ, инспекции и так далее.
Динамическое тестирование для получения корректных результатов требует исполнять код. Например, для модульных тестов, интеграционных, системных, приёмочных и прочих тестов. То есть тестирование проводится с использованием динамических данных, входных и выходных.
«Ящичный» подход
Согласно этому подходу, все тесты ПО делятся на три вида ящиков:
- Тестирование типа «белый ящик» проверяет внутренние структуры и модули, игнорирует ожидаемую функциональность для конечных пользователей. Это может быть тестирование API, внесение неисправностей (fault injection), модульное тестирование, интеграционное тестирование.
- Тестирование типа «чёрный ящик» больше интересуется тем, что делает ПО, а не как делает. Это означает, что тестировщики не обязаны ни разбираться в объекте тестирования, ни понимать, как он работает под капотом. Такой тип тестирования нацелен на конечных пользователей, их опыт взаимодействия с видимым интерфейсом. К «чёрным ящикам» относится тестирование на основе моделей, тестирование способов использования, таблицы переходов состояний, спецификационное тестирование и т. д.
- Тестирование типа «серый ящик» проектируется со знанием программных алгоритмов и структур данных (белый ящик), но выполняется на пользовательском уровне (чёрный ящик). Сюда относится регрессионное тестирование и шаблонное тестирование (pattern testing).
Теперь, чтобы запутать вас, скажу, что модульное тестирование может относиться и к «чёрному ящику», поскольку вы можете разбираться в тестируемом модуле, но не во всей системе. Хотя для меня оно по-прежнему «белый ящик», и предлагаю вам с этим согласиться.
Уровни тестирования
Их количество варьируется, обычно, в диапазоне от 4 до 6, и все они полезны. Названия тоже бывают разные, в зависимости от принятой в компании культуры вы можете знать «интеграционные» тесты как «функциональные», «системные» тесты как «автоматизированные», и так далее. Для простоты я опишу 5 уровней:
- Модульное тестирование.
- Интеграционное тестирование.
- Тестирование интерфейсов компонентов.
- Системное тестирование.
- Эксплуатационное приёмочное тестирование.
Модульное тестирование проверяет функциональность конкретного куска кода, обычно по одной функции за раз. Интеграционное тестирование проверяет интерфейсы между компонентами, чтобы собранные воедино модули формировали систему, работающую, как задумано. Это важный момент, потому что большое количество тестов, которые называют модульными, на самом деле являются интеграционными тестами, а разработчики считают их модулями. Если подразумевается использование нескольких модулей — это тестирование интеграции между ними, а не самих модулей. Тестирование интерфейсов компонентов проверяет данные, передаваемые между разными модулями. Например, получили данные из модуля 1 — проверили — передали в модуль 2 — проверили. Системное тестирование — это сквозное тестирование ради проверки соблюдения всех требований. Эксплуатационное приёмочное тестирование выполняется для проверки готовности к эксплуатации. Оно не является функциональным, проверяется лишь работоспособность сервисов, не повреждают ли какие-то подсистемы среду и прочие сервисы.
Типы тестирования
Каждый тип тестирования, вне зависимости от его уровня, также может подразделяться на другие типы. Существует больше 20 общепринятых типов. Самые распространённые:
- Регрессионное тестирование.
- Приёмочное тестирование.
- Дымовое (smoke) тестирование.
- UAT
- Разрушительное (Destructive) тестирование.
- Тестирование производительности.
- Непрерывное тестирование.
- Usability-тестирование.
- Тестирование безопасности.
Из названия понятно, для чего предназначен тот или иной тип тестирования. Жирным выделены модульные тесты в PHP. Если очень хочется, то к модульному тестированию можно применить каждый из этих терминов. Однако главной разновидностью модульных тестов являются тесты регрессионные, которые проверяют, все ли модули системы исполняются корректно после внесения изменений в код.
Теперь вы знаете, что модульные тесты являются динамическими, относятся к классу «белый ящик», выполняются на уровне модулей, представляют собой регрессионные тесты, но при этом под модульными тестами можно понимать многие разновидности тестов. Так что же такое на самом деле модульные тесты?
Что такое модульное тестирование?
V-модель — это графическое представление вышеупомянутых уровней, типов и их назначения в жизненном цикле разработки ПО.
После проверки и утверждения подробных требований к продукту, когда уже начали писать код, первой линией защиты от любых несоответствий становятся модульные тесты. Поэтому компании, понимающие, что они делают, заставляют разработчиков использовать модульные тесты или даже TDD, поскольку гораздо дешевле исправить баги на начальных этапах, чем на более поздних.
И это справедливо. У модульных тестов масса достоинств. Они:
- Изолируют каждую часть программы и проверяют её корректность.
- Помогают рано обнаруживать проблемы.
- Заставляют разработчиков мыслить в рамках входных, выходных и ошибочных условий.
- Придают коду удобный для тестирования вид, облегчают будущий рефакторинг.
- Упрощают интегрирование рабочих (!) модулей.
- Частично заменяют техническую документацию.
- Заставляют отделять интерфейс от реализации.
- Доказывают, что код модуля работает так, как ожидалось (хотя бы математически).
- Могут использоваться как низкоуровневые наборы регрессионных тестов.
- Демонстрируют прогресс в незавершённой системной интеграции.
- Снижают стоимость исправления багов (с TDD — ещё больше).
- Позволяют улучшать архитектуру приложения с помощью определения ответственности модулей.
- Если вы можете это протестировать, то можете присоединить к своей системе.
- Модульное тестирование — это ВЕСЕЛО!
Однако, есть определённые ограничения, о которых вы подумали, вероятно, при чтении этого списка:
- Модульное тестирование не вылавливает ошибки интегрирования.
- Каждое булево выражение требует как минимум двух тестов, и количество быстро растёт.
- Модульные тесты столь же глючные, как и тестируемый ими код.
- Привязка тестов к паре конкретных фреймворков или библиотек может ограничить рабочий процесс.
- Большинство тестов пишется после завершения разработки. Печально. Используйте TDD!
- Возможно, после маленького рефакторинга система будет работать как прежде, но тесты будут сбоить.
- Вырастает стоимость разработки.
- Человеческая ошибка: комментирование сломанных тестов.
- Человеческая ошибка: добавление в код обходных путей специально для прохождения модульных тестов.
Последнее убивает меня больше всего. (Почти) в каждом проекте прямо в исходном коде рабочего приложения я нахожу строки наподобие «если это модульный тест, грузить суррогатную SQLite базу данных, в противном случае грузить другую БД», или «если это модульный тест, не отправлять письмо, в противном случае отправлять», и так далее. Если у вашего приложения плохая архитектура, не притворяйтесь, что можете исправить паршивое ПО с помощью хорошего прохождения тестов, оно от этого не станет лучше.
Я часто обсуждал с коллегами и клиентами, что такое хороший модульный тест. Он:
- Быстрый.
- Автоматизированный.
- Полностью управляет всеми своими зависимостями.
- Надёжен: может запускаться в любом порядке, вне зависимости от других тестов.
- Может запускаться только в памяти (никаких взаимодействий с БД, чтений/записей в файловой системе).
- Всегда возвращает один результат.
- Удобен для чтения и сопровождения.
- Не тестирует SUT-конфигурацию (system under test).
- Имеет чётко определённую ЕДИНСТВЕННУЮ ЗАДАЧУ.
- Хорошо именован (и достаточно понятно, чтобы избежать отладки только ради выяснения, что же сбоит).
Тем, кто ухмыльнулся, прочитав «автоматизированный»: я не имел в виду интегрирование PHPUnit или JUnit в CI-конвейеры. Речь идёт о том, что если вы меняете код, сохраняете его и не знаете, проходят ли модули свои тесты, то они не автоматизированы, а должны бы. Выигрышный вариант — отслеживание файлов (File watcher).
Что нужно подвергать модульному тестированию?
В нормальных системах модульные тесты нужно писать для:
- Модулей — неделимых изолированных частей системы, которые выполняют какую-то одну задачу (функция, метод, класс).
- Публичных методов.
- Защищённых методов, но только в редких случаях и когда никто не видит.
- Багов и их исправлений.
Определение модульного теста зависит от разработчика, написавшего код. В PHP это почти всегда метод класса или функция, потому что это неделимая часть ПО, имеющая смысл сама по себе. Несколько раз я видел, как разработчики в качестве одного модуля использовали массив из однометодных миниклассов. Это имеет смысл, если минимальная функциональность требует наличия нескольких объектов.
Так что вы сами можете определять, что для вас является модулем. Или можете тестировать методы один за другим, упростив жизнь тому парню, что потом будет работать с кодом.
Если вы не проводите модульное тестирование, предлагаю заняться этим после возникновения следующего большого бага. Проверьте, с каким методом он будет связан, напишите сбойный тест с правильными аргументами и результатом, исправьте баг, снова запустите модульный тест. Если он будет пройден, то можете быть уверены, что этот баг пришлось исправлять в последний раз (с учётом ваших определённых входных сценариев).
Такой подход помогает легче понять модульное тестирование. Проанализируйте отдельно каждый метод. Поставщики данных могут помочь определить входные и выходные данные для любых сценариев, которые могут прийти вам в голову, поэтому что бы ни произошло, вы будете знать, чего ожидать.
Что НЕ нужно тестировать
Чуть сложнее определить, что тестировать не нужно. Я постарался собрать список элементов, которые не нужно подвергать модульному тестированию:
- Функциональность за пределами контекста (scope) модулей (!)
- Интеграция модулей с другими модулями (!)
- Неизолированное поведение (неимитируемые (unmockable) зависимости, настоящие БД, сеть)
- Приватные, защищённые методы.
- Статичные методы.
- Внешние библиотеки.
- Ваш фреймворк.
Уверен, не следует применять модульное тестирование ни к чему из вышеперечисленного, кроме статичных методов. Мне нравится аргументировать, что статичность, по сути, означает процедуральность, причём в многих случаях процедуральность глобальную. Если статичный метод вызывает другой статичный метод, то эту зависимость нельзя переопределить. А это значит, что вы теперь тестируете не изолированно. И тогда это уже не модульное тестирование. С другой стороны, это же часть кода, которая может жить сама по себе, у неё есть предназначение, и её нужно тестировать, чтобы удостовериться: какую бы часть этой бестолковой системы ни вызвала тестируемая часть кода, та не сломается. Поэтому считаю, что тестировать статичные методы можно, если вы уверены, что выходные данные вашего теста не сможет изменить никакой другой тест, и что язык или фреймворк позволят тестировать нативно.
Как писать модульные тесты?
- Пишите код, пригодный для модульного тестирования, затем тестируйте его.
- Пишите код, пригодный для модульного тестирования, затем тестируйте его.
- Пишите код, пригодный для модульного тестирования, затем тестируйте его.
Если «затем тестируйте его» недостаточно, то на laracasts.com есть очень хорошие видео про модульное тестирование PHP. Есть и масса сайтов, посвящённых той же задаче в других языках. Не вижу смысла объяснять, как я выполняю модульное тестирование, потому что инструменты меняются довольно быстро, и когда вы прочитаете этот текст, я могу переключиться с PHPUnit на Kahlan. Или нет. Кто знает.
Но ответить на первый вопрос (как писать код, пригодный для модульного тестирования) гораздо легче, и вряд ли ситуация сильно изменится со временем:
- SOLID
- DRY
- Отсутствие новых ключевых слов в конструкторе.
- Отсутствие циклов в конструкторе (и переходов, если это оговаривается).
- Отсутствие статичных методов, параметров, классов.
- Отсутствие методов setup(): объекты должны быть полностью инициализированы после конструирования.
- Отсутствие синглтонов (глобального состояния) и прочих нетестируемых антипаттернов.
- Отсутствие всемогущих объектов (God objects).
- Отсутствие классов со смешанной функциональностью (mixed concern classes).
- Отсутствие скрытых зависимостей.
Теперь, зная, чем являются и чем не являются модульные тесты, что нужно и что не нужно тестировать, какое место занимают модульные тесты в жизненном цикле разработки ПО, вам будет легче реализовывать их. Осталось найти фреймворк или библиотеку по душе. Если сомневаетесь, берите фреймворк/язык, ставший стандартом де-факто.
В заключение: модульные тесты очень важны как для разработчиков, так и для бизнеса. Их нужно писать, существуют отработанные методики, которые помогут вам легко покрыть модули тестами, в основном с помощью подготовки самих модулей. Но все эти методики не имеют смысла без знания теории тестирования, описанной в этой статье. Нужно уметь отличать модульные тесты от тестов других типов. И когда у вас в голове будет ясное понимание, то и писать тесты вам станет гораздо легче.
Вторым по важности аспектом тестирования после проектирования тестов является последовательность слияния всех модулей в систему или программу. Эта сторона вопроса обычно не получает достаточного внимания и часто рассматривается слишком поздно. Выбор этой последовательности, однако, является одним из самых жизненно важных решений, принимаемых на этапе тестирования, поскольку он определяет форму, в которой записываются тесты, типы необходимых инструментов тестирования, последовательность программирования модулей, а также тщательность и экономичность всего этапа тестирования. По этой причине такое решение должно приниматься на уровне проекта в целом и на достаточно ранней его стадии.
Тестирование модулей (или блоков) представляет собой процесс тестирования отдельных подпрограмм или процедур программы. Здесь подразумевается, что, прежде чем начинать тестирование программы в целом, следует протестировать отдельные небольшие модули, образующие эту программу. Такой подход мотивируется тремя причинами. Во–первых, появляется возможность управлять комбинаторикой тестирования, поскольку первоначально внимание концентрируется на небольших модулях программы. Во–вторых, облегчается задача отладки программы, т.е. обнаружение места ошибки и исправление текста программы. В–третьих, допускается параллелизм, что позволяет одновременно тестировать несколько модулей.
Цель тестирования модулей – сравнение функций, реализуемых модулем, со спецификациями его функций или интерфейса.
Тестирование модулей в основном ориентировано на принцип «белого ящика». Это объясняется, прежде всего, тем, что принцип «белого ящика» труднее реализовать при переходе в последующем к тестированию более крупных единиц, например программ в целом. Кроме того, последующие этапы тестирования ориентированы на обнаружение ошибок различного типа, т. е. ошибок, не обязательно связанных с логикой программы, а возникающих, например, из–за несоответствия программы требованиям пользователя.
Имеется большой выбор возможных подходов, которые могут быть использованы для слияния модулей в более крупные единицы. В большинстве своем они могут рассматриваться как варианты шести основных подходов: пошаговое тестирование; восходящее тестирование; нисходящее тестирование; метод «большого скачка»; метод сандвича; модифицированный метод сандвича.
Метод сандвича.Тестирование методом сандвича представляет собой компромисс между восходящим и нисходящим подходами. Здесь делается попытка воспользоваться достоинствами обоих методов, избежав их недостатков. При использовании этого метода одновременно начинают восходящее и нисходящее тестирование, собирая программу снизу и сверху, встречаясь где– то в середине. Точка встречи зависит от конкретной тестируемой программы и должна быть заранее определена при изучении ее структуры. Например, если разработчик может представить свою систему в виде уровня прикладных модулей, затем уровня модулей обработки запросов, затем уровня примитивных функций, то он может решить применять нисходящий метод на уровне прикладных модулей (программируя заглушки вместо модулей обработки запросов), а на остальных уровнях применить восходящий метод. Метод сандвича сохраняет такое достоинство нисходящего и восходящего подходов, как начало интеграции системы на самом раннем этапе. Поскольку вершина программы вступает в строй рано, мы, как в нисходящем методе, уже на раннем этапе получаем работающий каркас программы. Так как нижние уровни программы создаются восходящим методом, снимаются проблемы нисходящего метода, которые были связаны с невозможностью тестировать некоторые условия в глубине программы.
Модифицированный метод сандвича.При тестировании методом сандвича возникает та же проблема, что и при нисходящем подходе, но не так остро. Проблема эта в том, что невозможно досконально тестировать отдельные модули. Восходящий этап тестирования по методу сандвича решает эту проблему для модулей нижних уровней, но она может по– прежнему оставаться открытой для нижней половины верхней части программы. В модифицированном методе сандвича нижние уровни также тестируются строго снизу вверх. А модули верхних уровней сначала тестируются изолированно, а затем собираются нисходящим методом. Таким образом, модифицированный метод сандвича также представляет собой компромисс между восходящим и нисходящим подходами.
Восходящее тестирование.Программа собирается и тестируется «снизу вверх». Только модули самого нижнего уровня («терминальные» модули; модули, не вызывающие других модулей) тестируются изолированно, автономно. После того как тестирование этих модулей завершено, вызов их должен быть так же надежен, как вызов встроенной функции языка или оператор присваивания. Затем тестируются модули, непосредственно вызывающие уже проверенные. Эти модули более высокого уровня тестируются не автономно, а вместе с уже проверенными модулями более низкого уровня. Процесс повторяется до тех пор, пока не будет достигнута вершина. Здесь завершается и тестирование модулей, и тестирование сопряжений программы.
При восходящем тестировании для каждого модуля необходим драйвер: нужно подавать тесты в соответствии с сопряжением тестируемого модуля. Одно из возможных решений – написать для каждого модуля небольшую ведущую программу. Тестовые данные представляются как «встроенные» непосредственно в эту программу переменные и структуры данных, и она многократно вызывает тестируемый модуль, с каждым вызовом передавая ему новые тестовые данные. Имеется и лучшее решение: воспользоваться программой тестирования модулей – это инструмент тестирования, позволяющий описывать тесты на специальном языке и избавляющий от необходимости писать драйверы.
Здесь отсутствуют проблемы, связанные с невозможностью или трудностью создания всех тестовых ситуаций, характерные для нисходящего тестирования. Драйвер как средство тестирования применяется непосредственно к тому модулю, который тестируется, где нет промежуточных модулей, которые следует принимать во внимание. Анализируя другие проблемы, возникающие при нисходящем тестировании, можно заметить, что при восходящем тестировании невозможно принять неразумное решение о совмещении тестирования с проектированием программы, поскольку нельзя начать тестирование до тех пор, пока не спроектированы модули нижнего уровня. Не существует также и трудностей с незавершенностью тестирования одного модуля при переходе к тестированию другого, потому что при восходящем тестировании с применением нескольких версий заглушки нет сложностей с представлением тестовых данных.
Нисходящее тестирование.Нисходящее тестирование (называемое также нисходящей разработкой) не является полной противоположностью восходящему, но в первом приближении может рассматриваться как таковое. При нисходящем подходе программа собирается и тестируется «сверху вниз». Изолированно тестируется только головной модуль. После того как тестирование этого модуля завершено, с ним соединяются (например, редактором связей) один за другим модули, непосредственно вызываемые им, и тестируется полученная комбинация. Процесс повторяется до тех пор, пока не будут собраны и проверены все модули.
При этом подходе возникают два вопроса: 1. «Что делать, когда тестируемый модуль вызывает модуль более низкого уровня (которого в данный момент еще не существует)?» и 2. «Как подаются тестовые данные?»
Ответ на первый вопрос состоит в том, что для имитации функций недостающих модулей программируются модули – «заглушки», которые моделируют функции отсутствующих модулей.
Интересен и второй вопрос: в какой форме готовятся тестовые данные и как они передаются программе? Если бы головной модуль содержал все нужные операции ввода и вывода, ответ был бы прост: тесты пишутся в виде обычных для пользователей внешних данных и передаются программе через выделенные ей устройства ввода. Так, однако, случается редко. В хорошо спроектированной программе физические операции ввода– вывода выполняются на нижних уровнях структуры, поскольку физический ввод– вывод – абстракция довольно низкого уровня. Поэтому для того, чтобы решить проблему экономически эффективно, модули добавляются не в строго нисходящей последовательности (все модули одного горизонтального уровня, затем модули следующего уровня), а таким образом, чтобы обеспечить функционирование операций физического ввода– вывода как можно быстрее. Когда эта цель достигнута, нисходящее тестирование получает значительное преимущество: все дальнейшие тесты готовятся в той же форме, которая рассчитана на пользователя.
Нисходящий метод имеет как достоинства, так и недостатки по сравнению с восходящим. Самое значительное достоинство – то, что этот метод совмещает тестирование модуля, тестирование сопряжений и частично тестирование внешних функций. С этим же связано другое его достоинство: когда модули ввода– вывода уже подключены, тесты можно готовить в удобном виде. Нисходящий подход выгоден также в том случае, когда есть сомнения относительно осуществимости программы в целом или когда в проекте программы могут оказаться серьезные дефекты.
Преимуществом нисходящего подхода очень часто считают отсутствие необходимости в драйверах; вместо драйверов вам просто следует написать «заглушки».
Нисходящий метод тестирования имеет, к сожалению, некоторые недостатки. Основным из них является то, что модуль редко тестируется досконально сразу после его подключения. Дело в том, что основательное тестирование некоторых модулей может потребовать крайне изощренных заглушек. Программист часто решает не тратить массу времени на их программирование, а вместо этого пишет простые заглушки и проверяет лишь часть условий в модуле. Он, конечно, собирается вернуться и закончить тестирование рассматриваемого модуля позже, когда уберет заглушки. Такой план тестирования – определенно не лучшее решение, поскольку об отложенных условиях часто забывают.
Второй тонкий недостаток нисходящего подхода состоит в том, что он может породить веру в возможность начать программирование и тестирование верхнего уровня программы до того, как вся программа будет полностью спроектирована. Эта идея на первый взгляд кажется экономичной, но обычно дело обстоит совсем наоборот. Большинство опытных проектировщиков признает, что проектирование программы – процесс итеративный. Редко первый проект оказывается совершенным. Нормальный стиль проектирования структуры программы предполагает по окончании проектирования нижних уровней вернуться назад и подправить верхний уровень, внеся в него некоторые усовершенствования или исправляя ошибки, либо иногда даже выбросить проект и начать все сначала, потому что разработчик внезапно увидел лучший подход. Если же головная часть программы уже запрограммирована и оттестирована, то возникает серьезное сопротивление любым улучшениям ее структуры. В конечном итоге за счет таких улучшений обычно можно сэкономить больше, чем те несколько дней или недель, которые рассчитывает выиграть проектировщик, приступая к программированию слишком рано.
Метод «большого скачка».Вероятно, самый распространенный подход к интеграции модулей – метод «большого скачка». В соответствии с этим методом каждый модуль тестируется автономно. По окончании тестирования модулей они интегрируются в систему все сразу. Метод «большого скачка» по сравнению с другими подходами имеет много недостатков и мало достоинств. Заглушки и драйверы необходимы для каждого модуля. Модули не интегрируются до самого последнего момента, а это означает, что в течение долгого времени серьезные ошибки в сопряжениях могут остаться необнаруженными. Если программа мала (как, например, программа загрузчика) и хорошо спроектирована, метод «большого скачка» может оказаться приемлемым. Однако для крупных программ метод «большого скачка» обычно губителен.
Пошаговое тестирование.Реализация процесса тестирования модулей опирается на два ключевых положения: построение эффективного набора тестов и выбор способа, посредством которого модули комбинируются при построении из них рабочей программы. Второе положение является важным, так как оно задает форму написания тестов модуля, типы средств, используемых при тестировании, порядок кодирования и тестирования модулей, стоимость генерации тестов и стоимость отладки. Рассмотрим два подхода к комбинированию модулей: пошаговое и монолитное тестирование.
Возникает вопрос: «Что лучше – выполнить по отдельности тестирование каждого модуля, а затем, комбинируя их, сформировать рабочую программу или же каждый модуль для тестирования подключать к набору ранее оттестированных модулей?». Первый подход обычно называют монолитным методом, или методом «большого удара», при тестировании и сборке программы; второй подход известен как пошаговый метод тестирования или сборки.
Метод пошагового тестирования предполагает, что модули тестируются не изолированно друг от друга, а подключаются поочередно для выполнения теста к набору уже ранее оттестированных модулей. Пошаговый процесс продолжается до тех пор, пока к набору оттестированных модулей не будет подключен последний модуль.
Детального разбора обоих методов мы делать не будем, приведем лишь некоторые общие выводы.
1.Монолитное тестирование требует больших затрат труда. При пошаговом же тестировании «снизу– вверх» затраты труда сокращаются.
2.Расход машинного времени при монолитном тестировании меньше.
3.Использование монолитного метода предоставляет большие возможности для параллельной организации работы на начальной фазе тестирования (тестирования всех модулей одновременно). Это положение может иметь важное значение при выполнении больших проектов, в которых много модулей и много исполнителей, поскольку численность персонала, участвующего в проекте, максимальна на начальной фазе.
4.При пошаговом тестировании раньше обнаруживаются ошибки в интерфейсах между модулями, поскольку раньше начинается сборка программы. В противоположность этому при монолитном тестировании модули «не видят друг друга» до после дней фазы процесса тестирования.
5.Отладка программ при пошаговом тестировании легче. Если есть ошибки в межмодульных интерфейсах, а обычно так и бывает, то при монолитном тестировании они могут быть обнаружены лишь тогда, когда собрана вся программа. В этот момент локализовать ошибку довольно трудно, поскольку она может находиться в любом месте программы. Напротив, при пошаговом тестировании ошибки такого типа в основном связаны с тем модулем, который подключается последним.
6.Результаты пошагового тестирования более совершенны.
В заключение отметим, что п. 1, 4, 5, 6 демонстрируют преимущества пошагового тестирования, а п. 2 и 3 – его недостатки. Поскольку для современного этапа развития вычислительной техники характерны тенденции к уменьшению стоимости аппаратуры и увеличению стоимости труда, последствия ошибок в математическом обеспечении весьма серьезны, а стоимость устранения ошибки тем меньше, чем раньше она обнаружена; преимущества, указанные в п. 1, 4, 5, 6, выступают на первый план. В то же время ущерб, наносимый недостатками (п. 2 и 3), невелик. Все это позволяет нам сделать вывод, что пошаговое тестирование является предпочтительным.
Убедившись в преимуществах пошагового тестирования перед монолитным, исследуем две возможные стратегии тестирования: нисходящее и восходящее. Прежде всего внесем ясность в терминологию.
Во– первых, термины «нисходящее тестирование», «нисходящая разработка», «нисходящее проектирование» часто используются как синонимы. Действительно, термины «нисходящее тестирование» и «нисходящая разработка» являются синонимами (в том смысле, что они подразумевают определенную стратегию при тестировании и создании текстов модулей), но нисходящее проектирование – это совершенно иной и независимый процесс. Программа, спроектированная нисходящим методом, может тестироваться и нисходящим, и восходящим методами.
Во– вторых, восходящая разработка, или тестирование, часто отождествляется с монолитным тестированием. Это недоразумение возникает из– за того, что начало восходящего тестирования идентично монолитному при тестировании нижних или терминальных модулей. Но выше мы показали, что восходящее тестирование на самом деле представляет собой пошаговую стратегию.
Заказать ✍️ написание работы
В этой главе излагаются вопросы, связанные с проведением тестирования на всех этапах конструирования программной системы. Классический процесс тестирования обеспечивает проверку результатов, полученных на каждом этапе разработки. Как правило, он начинается с тестирования в малом, когда проверяются программные модули, продолжается при проверке объединения модулей в систему и завершается тестированием в большом, при котором проверяются соответствие программного продукта требованиям заказчика и его взаимодействие с другими компонентами компьютерной системы. Данная глава последовательно описывает содержание каждого шага тестирования. Здесь же рассматривается организация отладки ПО, которая проводится для устранения выявленных при тестировании ошибок.
Методика тестирования программных систем
Процесс тестирования объединяет различные способы тестирования в спланированную последовательность шагов, которые приводят к успешному построению программной системы (ПС) [3], [13], [64], [69]. Методика тестирования ПС может быть представлена в виде разворачивающейся спирали (рис. 8.1).
В начале осуществляется тестирование элементов (модулей), проверяющее результаты этапа кодирования ПС. На втором шаге выполняется тестирование интеграции, ориентированное на выявление ошибок этапа проектирования ПС. На третьем обороте спирали производится тестирование правильности, проверяющее корректность этапа анализа требований к ПС. На заключительном витке спирали проводится системное тестирование, выявляющее дефекты этапа системного анализа ПС.
Охарактеризуем каждый шаг процесса тестирования.
1. Тестирование элементов. Цель — индивидуальная проверка каждого модуля. Используются способы тестирования «белого ящика».
Рис. 8.1. Спираль процесса тестирования ПС
2. Тестирование интеграции. Цель — тестирование сборки модулей в программную систему. В основном применяют способы тестирования «черного ящика».
3. Тестирование правильности. Цель — проверить реализацию в программной системе всех функциональных и поведенческих требований, а также требования эффективности. Используются исключительно способы тестирования «черного ящика».
4. Системное тестирование. Цель — проверка правильности объединения и взаимодействия всех элементов компьютерной системы, реализации всех системных функций.
Организация процесса тестирования в виде эволюционной разворачивающейся спирали обеспечивает максимальную эффективность поиска ошибок. Однако возникает вопрос — когда заканчивать тестирование?
Ответ практика обычно основан на статистическом критерии: «Можно с 95%-ной уверенностью сказать, что провели достаточное тестирование, если вероятность безотказной работы ЦП с программным изделием в течение 1000 часов составляет по меньшей мере 0,995».
Научный подход при ответе на этот вопрос состоит в применении математической модели отказов. Например, для логарифмической модели Пуассона формула расчета текущей интенсивности отказов имеет вид:

где 

С помощью уравнения (8.1) можно предсказать снижение ошибок в ходе тестирования, а также время, требующееся для достижения допустимо низкой интенсивности отказов.
Тестирование элементов
Объектом тестирования элементов является наименьшая единица проектирования ПС — модуль. Для обнаружения ошибок в рамках модуля тестируются его важнейшие управляющие пути. Относительная сложность тестов и ошибок определяется как результат ограничений области тестирования элементов. Принцип тестирования — «белый ящик», шаг может выполняться для набора модулей параллельно.
Тестированию подвергаются:
q интерфейс модуля;
q внутренние структуры данных;
q независимые пути;
q пути обработки ошибок;
q граничные условия.
Интерфейс модуля тестируется для проверки правильности ввода-вывода тестовой информации. Если нет уверенности в правильном вводе-выводе данных, нет смысла проводить другие тесты.
Исследование внутренних структур данных гарантирует целостность сохраняемых данных.
Тестирование независимых путей гарантирует однократное выполнение всех операторов модуля. При тестировании путей выполнения обнаруживаются следующие категории ошибок: ошибочные вычисления, некорректные сравнения, неправильный поток управления [3].
Наиболее общими ошибками вычислений являются:
1) неправильный или непонятый приоритет арифметических операций;
2) смешанная форма операций;
3) некорректная инициализация;
4) несогласованность в представлении точности;
5) некорректное символическое представление выражений.
Источниками ошибок сравнения и неправильных потоков управления являются:
1) сравнение различных типов данных;
2) некорректные логические операции и приоритетность;
3) ожидание эквивалентности в условиях, когда ошибки точности делают эквивалентность невозможной;
4) некорректное сравнение переменных;
5) неправильное прекращение цикла;
6) отказ в выходе при отклонении итерации;
7) неправильное изменение переменных цикла.
Обычно при проектировании модуля предвидят некоторые ошибочные условия. Для защиты от ошибочных условий в модуль вводят пути обработки ошибок. Такие пути тоже должны тестироваться. Тестирование путей обработки ошибок можно ориентировать на следующие ситуации:
1) донесение об ошибке невразумительно;
2) текст донесения не соответствует, обнаруженной ошибке;
3) вмешательство системных средств регистрации аварии произошло до обработки ошибки в модуле;
4) обработка исключительного условия некорректна;
5) описание ошибки не позволяет определить ее причину.
И, наконец, перейдем к граничному тестированию. Модули часто отказывают на «границах». Это означает, что ошибки часто происходят:
1) при обработке n-го элемента n-элементного массива;
2) при выполнении m-й итерации цикла с т проходами;
3) при появлении минимального (максимального) значения.
Тестовые варианты, ориентированные на данные ситуации, имеют высокую вероятность обнаружения ошибок.
Тестирование элементов обычно рассматривается как дополнение к этапу кодирования. Оно начинается после разработки текста модуля. Так как модуль не является автономной системой, то для реализации тестирования требуются дополнительные средства, представленные на рис. 8.2.
Рис. 8.2.Программная среда для тестирования модуля
Дополнительными средствами являются драйвер тестирования и заглушки. Драйвер — управляющая программа, которая принимает исходные данные (InData) и ожидаемые результаты (ExpRes) тестовых вариантов, запускает в работу тестируемый модуль, получает из модуля реальные результаты (OutData) и формирует донесения о тестировании. Алгоритм работы тестового драйвера приведен на рис. 8.3.
Рис. 8.3.Алгоритм работы драйвера тестирования
Заглушки замещают модули, которые вызываются тестируемым модулем. Заглушка, или «фиктивная подпрограмма», реализует интерфейс подчиненного модуля, может выполнять минимальную обработку данных, имитирует прием и возврат данных.
Создание драйвера и заглушек подразумевает дополнительные затраты, так как они не поставляются с конечным программным продуктом.
Если эти средства просты, то дополнительные затраты невелики. Увы, многие модули не могут быть адекватно протестированы с помощью простых дополнительных средств. В этих случаях полное тестирование может быть отложено до шага тестирования интеграции (где драйверы или заглушки также используются).
Тестирование элемента просто осуществить, если модуль имеет высокую связность. При реализации модулем только одной функции количество тестовых вариантов уменьшается, а ошибки легко предсказываются и обнаруживаются.
Тестирование интеграции
Тестирование интеграции поддерживает сборку цельной программной системы.
Цель сборки и тестирования интеграции: взять модули, протестированные как элементы, и построить программную структуру, требуемую проектом [3].
Тесты проводятся для обнаружения ошибок интерфейса. Перечислим некоторые категории ошибок интерфейса:
q потеря данных при прохождении через интерфейс;
q отсутствие в модуле необходимой ссылки;
q неблагоприятное влияние одного модуля на другой;
q подфункции при объединении не образуют требуемую главную функцию;
q отдельные (допустимые) неточности при интеграции выходят за допустимый уровень;
q проблемы при работе с глобальными структурами данных.
Существует два варианта тестирования, поддерживающих процесс интеграции: нисходящее тестирование и восходящее тестирование. Рассмотрим каждый из них.
Воспользуйтесь поиском по сайту:
Говорят, что попытка запуска одного из первых искусственных спутников Земли закончилась неудачей, так как в одной из программ была неверно поставлена десятичная точка. Ошибки программирования редко приводят к столь серьезным последствиям, однако известно, что отыскание ошибок в логике программ стоит огромного количества времени и сил. Средства, затрачиваемые на это, исчисляются миллионами долларов. Ошибки же в программах операционной системы приводят к значительным простоям дорогостоящего оборудования. Телефонная компания, использующая автоматизированную систему расчетов, может выплатить абоненту 7000 долларов, вместо того чтобы получить с него 7. А кому не приходилось встречаться с определенными недоразумениями, связанными с использованием ЭВМ для расчетов с помощью кредитных карточек?
Предыдущий раздел был посвящен описанию процедуры, используемой в тех случаях, когда обнаружение ошибки аппаратурой или операционной системой приводит к выдаче дампа. После того как все ошибки, приводящие к прекращению выполнения программы, устранены, появляется желание сказать: «Дампа нет, следовательно, программа работает правильно». Желание тем сильнее, чем скорее требуется закончить работу. Однако этого не следует говорить до тех пор, пока не будет проведено достаточно полное тестирование.
Модульное программирование
В большинстве случаев будет, по меньшей мере, неосторожно заключить, что программа не содержит ошибок, если она правильно выполняется и приводит к получению искомых результатов для одного тестового набора исходных данных. Более того, многие решаемые задачи настолько сложны, что тестирование путем задания некоторых стандартных исходных данных не дает никакой уверенности в отсутствии ошибок. Таким образом, наша задача сводится к определению, содержит ли данная программа или набор программ ошибки или нет. Если ошибки содержатся в самой логике программ (мы, естественно, сейчас исключаем ошибки, обнаруживаемые аппаратными средствами или операционной системой), то как организовать их поиск в достаточно сложных случаях?
Конечно, уже при составлении программы следует предусмотреть возможность возникновения описанных трудностей. Для этого обычно крупные программы подразделяются на более мелкие, так называемые модули, каждый из которых предназначен для решения узкой и специфичной задачи.
Идеальным, вообще говоря, является случай, когда таким образом составленные модули независимы в том смысле, что изменение, внесенное в один из них, никак не влияет на работу остальных. Это означает, что для устранения ошибки, содержащейся в одном из модулей, достаточно внести исправления только лишь в сам модуль. Полная независимость модулей практически почти никогда не достижима, тем не менее, некоторое приближение к идеальному случаю достигается при подготовке значительных по размеру программ в форме пакетов. Такие программы значительно удобнее как для составления, так и для откладки, чем те, которые пишутся без использования модульного принципа.
Во многих реальных случаях решаемая задача настолько сложна, что требует участия в работе довольно большого числа программистов. Обычно небольшая группа программистов выполняет составление, отладку и тестирование одного модуля. Каждой такой группе должно быть точно известно, какие требования предъявляются к составляемому ими модулю, какие исходные данные он должен обрабатывать, какие результаты и в каком формате должен получать. Подобный групповой способ программирования требует тщательной подготовки, планирования и управления всей работой, так чтобы каждая группа точно знала, что требуется непосредственно от нее.
Рис. 12.6. Блок-схема программы учета запасов.
Деление программы на модули в большинстве случаев очевидно, поскольку решаемая задача, как правило, состоит из нескольких основных частей. Обычно модули, предназначенные для выполнения некоторых логически связанных между собой функций, объединяются в так называемые подпрограммы. Каждая подпрограмма на самом деле представляет собой независимую программу и содержит, по крайней мере, один модуль.
Посмотрим теперь на рис. 12.6. На нем изображена общая блок- схема некоторой программы организации учета запасов, базирующейся на использовании памяти на магнитных дисках. В данном случае управляющей программой используются четыре подпрограммы: подпрограмма ввода с карт, подпрограмма печати, подпрограмма корректировки записей и, наконец, подпрограмма, организующая работу с памятью на магнитных дисках. В приведенной ниже таблице указаны подпрограммы и содержащиеся в них модули.
|
Подпрограмма |
Модули |
|
Ввод с карт |
1. Ввод с карт 2. Преобразование данных |
|
Печать |
1. Преобразование данных 2. Вывод на печать |
|
Корректировка записей |
1. Изменение общего количества 2. Изменение учетного номера 3. Изменение цены 4. Другие |
|
Работа с дисками |
1. Считывание текущей информации в память 2. Обновление информации 3. Запись на диск новых данных |
Центральная часть блок-схемы представляет собой управляющую
программу, функции которой состоят в осуществлении контроля за порядком выполнения подпрограмм и задании им исходных данных. Таким образом, задача основной управляющей программы состоит в интерпретации входных команд и реакции на них посредством запросов на выполнение соответствующих подпрограмм, вызовов подпрограмм (подробнее о подпрограммах см. в гл. 13).
Типичной операцией учета является изменение в ведомости общего количества товаров данного типа в связи с продажей некоторых из них. Для внесения соответствующего изменения в запись ведомости должны быть выполнены следующие действия:
1. Вызов управляющей программой подпрограммы ввода с перфокарт для считывания информации с управляющей карты. В состав этой информации входят учетный номер, код требуемого изменения и его количественное значение.
2. Считывание карты модулем ввода.
3. Проверка введенных данных модулем преобразования и преобразование числовой информации в форму, подходящую для последующего использования.
4. Проверка управляющей программой введенной управляющей информации, определение требуемой операции и вызов подпрограммы работы с дисками.
5. Считывание информации с диска модулем ввода подпрограммы работы с дисками.
6. Вызов подпрограммы корректировки.
7. Работа модуля изменения общего количества подпрограммы обработки записей.
8. Вызов управляющей программой подпрограммы обмена с дисками.
9. Возврат модифицированной информации на соответствующее место дисковой памяти модулем записи на диск новых данных вызванной подпрограммы.
10. Вызов подпрограммы печати, производящей преобразование формы представления данных и вывод этих данных на печать.
Заметим, что в рассмотренном примере каждый модуль выполняет свою специфическую функцию. Если бы программа учета была составлена как единое целое, т. е. без подразделения на модули и подпрограммы, то задача ее отладки стала бы значительно более сложной, если не невыполнимой. После устранения ошибок, обнаруженных ЭВМ или операционной системой, программа может получить некоторые результаты, но ведь они могут быть и неверны:
Предположим, например, что в результате выполнения учетной операции было выведено на печать отличающееся от истинного значение общего количества каких-то товаров. Может быть, управляющая карта содержит неправильные данные? А может быть, ошибка допущена при преобразовании формы представления информации? Или что-то не так с самой корректировкой?
Модульное программирование позволяет избежать встречи с подобной ситуацией. Каждый модуль пишется и, отлаживается отдельно. Затем модули объединяются в подпрограммы, и снова каждая подпрограмма проходит свой набор тестов. Только после полной отладки все модули и подпрограммы собираются вместе, и производится попытка организации их работы под управлением основной программы. Если предположить, что все подпрограммы работают правильно, то в случае общей неверной работы ошибки следует искать либо в управляющей программе, либо в организации обменов данными между подпрограммами.
Прежде чем перейти к подробному рассмотрению процессов отладки и тестирования модулей, укажем на еще одно преимущество модульного программирования. Разбиением программы на более мелкие части достигается значительное увеличение ее гибкости, т. е. возможности ее модификации. Предположим, что мы решили для ввода и вывода управляющей информации и выдачи результатов использовать терминал вместо устройства ввода с перфокарт и печати. Если модули, выполняющие преобразование данных, составлены как следует, то нам всего лишь требуется заменить модули считывания с карт и печати на модули ввода-вывода на терминал. Если мы хотим хранить» учетную информацию не на дисках, а на лентах, то модуль обмена с дисками следует заменить на модуль обмена с лентой. Для увеличения множества операций корректировки текущей информации в программу следует добавить новые модули, а также внести некоторые новые команды в подпрограмму модификации для обеспечения возможности работы этих вновь созданных введенных модулей.
Подобная гибкость требует практической независимости отдельных модулей, поэтому отладка и тестирование этих модулей должны также производиться независимо. Перейдем теперь к непосредственному рассмотрению вопросов тестирования и отладки модулей.
Тестирование и отладка модулей
Независимо от того, является ли данный модуль частью вновь создаваемой или должен быть введен в уже существующую систему, к отладке его следует приступать еще до начала перфорации. Попытайтесь определить, к каким результатам может привести задание вашей программе различных наборов исходных данных, учитывая, что, вообще говоря, не всегда исходная информация будет задаваться правильно. Дополните модуль командами, проверяющими корректность входных данных. Выполнение таких проверок полезно не только на этапе отладки, но и в процессе работы модуля как части некоторой общей системы. Внимательно просмотрите все циклы. Что в действительности будет происходить, если количество прохождений некоторого цикла должно быть равно 0? Существует ли ограничение на число прохождений цикла? Тщательно проверьте организацию всех передач управления. Такой предварительный просмотр программы несколько утомителен и может показаться малоэффективным, однако в большинстве случаев он позволяет впоследствии избежать довольно значительных дополнительных затрат.
Все примеры и упражнения данной главы достаточно просты для того, чтобы соответствующие программы могли состоять всего из одного модуля, включающего средства выполнения операций ввода и вывода. В более сложных случаях ввод и вывод обеспечивается далеко не в каждом модуле и далеко не каждый модуль представляет собой законченную программу. Примером может служить, например, модуль изменения общего количества товаров, изображенный на рис. 12.6.
В подобных ситуациях отладка и тестирование требуют дополнительного программирования.
Нашей целью является по возможности более точное моделирование условий работы каждого модуля. Если модуль не представляет собой отдельную законченную подпрограмму, то к нему следует временно добавить предложения, обеспечивающие связь подпрограмм, для создания возможности его использования в операционной системе. Разумеется, это совместное использование следует организовать так, чтобы оно не оказывало существенного влияния на процессы ввода, вывода и порядок вычислений в самом модуле. В противном случае после устранения временно внесенных предложений и включения модуля в состав некоторой крупной программы система в целом может оказаться неработоспособной.
Если ввод необходимой для работы модуля информации выполняется вне самого модуля, то необходимо составить так называемую управляющую программу, или генератор входных данных, которая обеспечивает передачу модулю входных данных в соответствующей форме. Например, модуль изменения общего количества товаров должен получать последовательность записей с диска, а кроме того, количественные значения изменений, которые следует произвести. В процессе отладки эти данные должны задаваться генератором входных данных.
Составление управляющей программы и выбор значений, которые должны получаться на ее выходе, вообще говоря, является нетривиальной задачей. Мы хотим проверить модуль достаточно полно. Генератор должен формировать совокупность входных данных, с помощью которых можно проверить все ветви модуля. Однако недостаточно отладить работу модуля со входными данными, удовлетворяющими нормальным условиям его применения. Нужно также проверить работу модуля со входными данными, либо содержащими ошибки, либо выходящими за пределы допустимой области значений.
В обычном случае модуль изменения общего количества производит сложение или вычитание заданного значения из числа, находящегося в поле общего количества обрабатываемой записи. Но следует предусмотреть возможность возникновения ненормальных ситуаций. Если в результате вычислений общее количество товаров получается отрицательным, что свидетельствует о наличии ошибки во входных данных или в модифицируемой записи, а возможно, о продаже отсутствующего товара, модулем должна быть зафиксирована особая ситуация (обычно это достигается посылкой сообщения об ошибке основной программе). Модуль должен проверить, что производится модификация именно той записи, которой нужно. Для этого следует сравнить учетный номер записи с номером, задаваемым управляющей картой. Если номера не совпадают, то нужно послать предупреждающее сообщение пользователю. Возможность возникновения различных ситуаций такого рода должна быть рассмотрена на этапах проектирования и программирования модуля, его отладки, тестирования, но ни в коем случае не после начала использования модуля в качестве составной части некоторого пакета.
В программах вычислительного типа ошибочные случаи должны быть рассмотрены с такой же тщательностью, как и нормальные. Возьмем в качестве примера программу обращения матриц. Предположим, что мы обратили вручную матрицу размером 3 на 3, затем использовали для обращения написанную программу и получили тот же результат. Вам следует четко представлять, что это только начало тестирования. К сожалению, некоторые программисты, успешно пройдя лишь этот первый этап, полагают, что составленные ими модули работают должным образом. Тем не менее, осталось еще несколько невыясненных вопросов. Мы вообще привыкли мыслить категориями трехмерного пространства, поэтому вполне вероятно предположить, что создаваемые нами программы гораздо лучше подходят для работы именно в трехмерных случаях. Итак, следует проверить правильность работы программы и тогда, когда размерность матрицы больше чем 3, и тогда, когда она меньше. Программа должна охватывать и такие специальные случаи, как обращение матриц размерностью 0x0, 1×1, 2×2. Также правильно должно выполняться обращение матриц с высокой размерностью.
Что можно сказать о вырожденных (необратимых) матрицах? Модуль должен производить проверку условия вырожденности и в случае его выполнения посылать соответствующее сообщение вызывающей программе и возвращать ей управление вместо попыток деления на 0, неизбежно производящихся при отсутствии подобных проверок. Одним из тестов должно служить задание матриц, близких к вырожденным (при обращении таких матриц обычно получаются значительные ошибки). Информация о прохождении программой всех тестов, ограничениях на размерность обращаемых матриц и величине ошибок, возникающих вследствие округлений, должна быть внесена в документацию, прилагаемую к модулю.
Короче говоря, независимо от области предполагаемого применения программы должны проверяться на работу не только с нормально заданной входной информацией, но и с ошибочными или выходящими за предусмотренные пределы данными. Иногда полезно исследовать и некоторые предельные случаи.
Если модуль не обеспечивает вывода на печать, то необходимо добавить соответствующие команды для обеспечения возможности проверки правильности его выполнения. Несколько раньше мы обсуждали использование дампа в случае фиксации ошибки самой машиной или операционной системой. В языке ассемблера эффективным способом получения результатов от программы, не выдающей ничего на печать, является запрос выдачи дампа. Для этой цели предусмотрена специальная макрокоманда ABEND, которая в общем виде выглядит следующим образом:
ABEND n, DUMP
Дамп, выдаваемый по команде ABEND, ничем не отличается JOT уже рассматривавшихся нами дампов. Параметр n представляет собой пользовательский код завершения, который появляется в распечатке вместе с дампом.
Макрокоманда ABEND вызывает прекращение выполнения программы и поэтому делает невозможным продолжение отладки модуля в пределах того же задания. Во многих конкретных вычислительных системах существуют средства, позволяющие распечатывать содержимое памяти и регистров по требованию, не вызывая прекращения выполнения основной программы. Вы можете пользоваться также своими собственными отладочными макро. Существует, кроме того, процедура PDUMP, входящая в набор стандартных процедур языка ФОРТРАН. Используя PDUMP, можно распечатать содержимое указанных областей памяти в одном из нескольких форматов. Вызывается эта процедура обычными командами вызова подпрограмм. Более подробно эти вопросы освещены в руководстве «Библиотека OS ФОРТРАН IV. Вычислительные и обслуживающие подпрограммы» (GC28—6818).
Не бойтесь выдачи большого количества промежуточной информации при отладке или тестировании программ. Следует использовать все находящиеся в нашем распоряжении возможности для облегчения процесса поиска ошибок. Однако, конечно же, не мешает подумать, какая конкретно информация вам нужна. Это может сэкономить значительное количество не только вашего, но и машинного времени.
Сборка программы
После того как модули окончательно проверены и отлажены, встает вопрос о компоновке из этих модулей программного пакета.
Если несколько модулей являются составными частями какой-то подпрограммы, то эта подпрограмма должна включать в себя предложения, определяющие, какие именно модули принадлежат ей. Например, подпрограмма обработки записей, изображенная на рис. 12.6, состоит из нескольких модулей. При вызове подпрограммы должно однозначно определяться, какие именно модули следует использовать. Эта цель обычно достигается выполнением команд, анализирующих управляющий код, задаваемый основной программой в виде одного из элементов входных данных.
Каждая подпрограмма должна быть отлажена точно таким же образом, что и входящие в нее модули. Это обычно требует разработки генераторов входных данных и программ вывода. Снова необходимо проверить правильность работы каждой подпрограммы и обеспечить диагностику возможных ошибок.
Наконец мы добрались до управляющей программы. Как правило, эта программа не производит никаких вычислений, она лишь определяет порядок выполнения подпрограмм и передает им соответствующие данные и управляющую информацию. Зная, какие действия выполняются отдельными модулями и подпрограммами, нетрудно, просмотрев основную программу, определить, как работает система в целом.
На рис. 12.7 приведена блок-схема управляющей программы системы, изображенной на рис. 12.6. Отметим, что весь ввод-вывод, вычисления и проверка на наличие ошибок производятся внутри подпрограмм; основная программа лишь определяет последовательность их выполнения.
Тестирование всей системы в целом имеет своей целью обеспечить правильность выполнения работ при допустимых входных данных и выдачу соответствующей диагностики в остальных случаях. Вероятно, хуже всего, если система никак не будет реагировать на неправильные данные. Лучше выдавать сообщения об ошибках в более широком, чем это требуется, классе случаев, чем допустить иллюзию правильной работы при наличии ошибок.
Совершенствование и поддержка
Чаще всего независимо от того, с какой тщательностью были проведены отладка и тестирование, система уже в готовом виде содержит некоторое количество ошибок. Это означает, что оставшиеся ошибки будут обнаруживаться и устраняться параллельно с процессом непосредственного использования системы. Изменения, вносимые в само оборудование или в программное обеспечение, с которым созданный нами пакет используется, часто влекут за собой необходимость внесения изменений в этот уже работающий пакет. Также достаточно часто в уже работающую систему вносятся некоторые дополнения. Поддержка систем, т. е. обеспечение возможности их использования в изменяющихся условиях, достигается посредством все того же модульного программирования и объединения этих модулей в пакеты. Добавление в систему новых возможностей в идеальном случае требует лишь добавления к ней новых модулей (конечно, после их отладки и тестирования).
Рис. 12.7. Блок-схема управляющей программы системы учета запасов.
Рискуя показаться навязчивым, я, тем не менее, снова указываю на необходимость подробного документирования всех программ. Документирование отлаживаемых и тестируемых программ и систем хотя и не является приятным, тем не менее, очень полезное занятие. Отсутствие такого рода документации не дает возможности учесть изменения, внесенные в программу на этапах отладки и тестирования. Документация должна обеспечивать пользователей всей информацией, необходимой для организации поддержки программы и внесения в нее необходимых дополнений.
Модульное тестирование является одним из основных видов тестирования программного обеспечения. Как и в случае с тестированием программного обеспечения, модульное тестирование не может гарантировать отсутствие ошибок после развертывания приложения. Однако, тестируя наименьшие повторяющиеся фрагменты кода в ваших приложениях, модульное тестирование помогает выявлять ошибки в строительных блоках вашего проекта до того, как они повлияют на ваше интегрированное приложение.
Будучи важным процессом разработки программного обеспечения, модульное тестирование является ценным навыком, который должен знать разработчик. Если бы мы могли следовать передовым методам модульного тестирования, мы уже проводили бы модульное тестирование на этапе разработки. Однако это не реально для всех проектов разработки программного обеспечения. Таким образом, модульное тестирование может выполняться после того, как ваш производственный код написан, и это необходимо для обеспечения того, чтобы расширенный и рефакторинговый код оставался функциональным.
Сегодня мы рассмотрим обзор модульного тестирования и лучших практик модульного тестирования.
Обзор модульного тестирования
Что такое модульное тестирование?
Модульное тестирование гарантирует, что модули в вашей программе работают должным образом. Поскольку человек, написавший фрагмент кода, лучше всего понимает его ожидаемое поведение, ответственность за модульное тестирование обычно лежит на разработчике. В сочетании со сквозными и интеграционными тестами модульное тестирование помогает обеспечить качество кода на ранних этапах процесса разработки.
Модуль — это наименьший фрагмент кода в вашей программе, который повторяем, тестируем и функционален. Единицами могут быть функции, классы, методы и так далее.
Уровни тестирования программного обеспечения
На предыдущем рисунке показано модульное тестирование наряду с другими уровнями тестирования программного обеспечения, где каждый уровень имеет свою область применения:
- Модульное тестирование проверяет каждый программный модуль.
- Интеграционное тестирование проверяет объединенные модули в соответствии с проектной документацией системы и функциональными требованиями.
- Функциональное тестирование проверяет желаемую функциональность программы на основе документов анализа требований и руководства пользователя (т. е. обеспечение ожидаемого результата для заданных входных данных). Это иногда делится на системное тестирование и приемочное тестирование.
Тестирование «черного ящика» и «белого ящика» — это два подхода к тестированию программного обеспечения, в которых:
- Тестирование черного ящика проверяет поведение программного обеспечения, не зная деталей реализации программного модуля. Эти тесты получены из документа спецификации программного обеспечения.
- Тестирование методом белого ящика проверяет фактическую реализацию программного модуля, при этом внутренняя архитектура программы известна тестировщику. Эти тесты дают нам представление о реализации для разработки более комплексных тестов.
Для модульного тестирования наиболее подходящим вариантом обычно является тестирование белого ящика, особенно когда наши модули меньше и их код легче понять. С другой стороны, тестирование «черного ящика» — хороший вариант для более поздних стадий проекта, когда модули были интегрированы для создания сложного программного обеспечения.
Преимущества модульного тестирования
Некоторые преимущества модульного тестирования включают в себя:
- Упрощает отладку : тестируя функциональность небольших модулей, вы можете обнаруживать ошибки до того, как они повлияют на другие модули в интегрированном приложении.
- Поощряет более слабосвязанный код : намеренно уменьшая взаимозависимости между модулями, мы получаем более слабосвязанный код, что является передовой практикой кодирования.
- Быстрее, чем функциональное тестирование : поскольку модули очень маленькие, вы можете запустить несколько модульных тестов за секунды (при условии, что вы их автоматизировали).
- Минимизация регрессии кода : после рефакторинга или расширения кода вы можете повторно запустить все наборы тестов, чтобы убедиться, что ваш новый или обновленный код не нарушает существующие функции.
- Лучшее покрытие кода. Разбивая приложение на мельчайшие тестируемые компоненты, модульное тестирование помогает увеличить покрытие кода.
Тестовое покрытие
Тестовое покрытие — это метрика, определяющая, насколько тщательно мы провели модульное тестирование нашего программного обеспечения. Как правило, мы хотим максимально увеличить тестовое покрытие.
Тестовое покрытие можно дополнительно классифицировать следующим образом:
- Охват функций: процент функций, которые были протестированы.
- Покрытие операторов: процент утверждений, которые были протестированы. Иногда он включает в себя все операторы, которые были выполнены хотя бы один раз во время тестирования.
- Покрытие пути: процент проверенных путей. Программное обеспечение может иметь несколько путей (или ответвлений), выбранных им в зависимости от условий, таких как ввод данных пользователем, определение среды и другие события.
Среды модульного тестирования
Платформа модульного тестирования — это программное обеспечение, которое позволяет нам быстро создавать модульные тесты и автоматизировать их выполнение. В случае сбоя теста фреймворки могут сохранить результаты или выдать утверждение.
Существуют десятки фреймворков для модульного тестирования, доступных для различных языков программирования. Некоторые популярные среды модульного тестирования включают Cunit, Moq, Cucumber, Selenium, Embunit, Sass True, HtmlUnit и JUnit (одна из наиболее широко используемых сред с открытым исходным кодом для языка программирования Java).
Мы можем сэкономить много времени и ресурсов с помощью фреймворков модульного тестирования, которые включают следующие функции:
- Набор тестов: набор тестовых случаев, сгруппированных для выполнения тестов, что позволяет нам объединять несколько похожих модульных тестов.
- Test runner: компонент, который автоматизирует выполнение серии тестов и возвращает результат теста пользователю.
- Приспособление для тестирования: предопределенное состояние или фиксированная среда, в которой мы запускаем тесты, гарантируя, что наши тесты повторяются при нескольких запусках тестов.
9 лучших практик модульного тестирования
1. Напишите тесты для ряда сценариев
При написании тестового примера убедитесь, что вы рассматриваете все возможные сценарии. Другими словами, не просто напишите тест на счастливый путь. Подумайте и о других сценариях, таких как обработка ошибок.
2. Напишите хорошие названия тестов
Хорошее имя модульного теста должно явно отражать цель тестового примера. Следуйте согласованным соглашениям об именах и используйте сокращения только в том случае, если они понятны читателю. Написание хороших имен тестов повышает читабельность кода, что облегчит вам и другим возможность расширять этот код в будущем.
3. Настройте автоматические тесты
Выберите автоматизированное модульное тестирование с помощью среды модульного тестирования. Еще лучше автоматизировать тесты в конвейере непрерывной интеграции (CI/CD).
Альтернативой автоматизированному тестированию является ручное тестирование, при котором мы вручную выполняем тестовые случаи и собираем их результаты. Как вы понимаете, ручное тестирование небольших модулей невероятно утомительно. Он также менее надежен. Автоматические тесты, безусловно, помогут вам в этом.
4. Пишите детерминированные тесты
Ложноположительные и отрицательные результаты являются обычным явлением при тестировании программного обеспечения, и мы должны приложить все усилия, чтобы свести их к минимуму. Цель состоит в том, чтобы иметь согласованные выходные данные для тестов, чтобы проверить желаемую функцию. Поэтому модульные тесты должны быть детерминированными. Другими словами, пока код теста не изменяется, детерминированный тест должен иметь стабильное поведение при каждом запуске теста.
5. Организовать, действовать и утверждать (ААА)
Протокол AAA является рекомендуемым подходом для структурирования модульных тестов. В качестве передовой практики модульного тестирования это улучшает читабельность вашего теста, придавая ему логическую последовательность. ААА иногда называют протоколом «Дано/Когда/Тогда».
Вы можете использовать протокол AAA для структурирования модульных тестов, выполнив следующие шаги:
- Упорядочить: Упорядочить настройку и инициализацию для теста.
- Act: Действуйте на единицу для данного теста.
- Утвердить: Утвердить или проверить результат
Следующий код демонстрирует структуру AAA при тестировании функции абсолютного значения в Python:
def test_abs_for_a_negative_number(): # Arrange negative = -2 # Act answer = abs(negative) # Assert assert answer == 2
6. Пишите тесты до или во время разработки
В идеальном мире мы пишем тестовый код перед написанием рабочего кода. Это известно как разработка через тестирование (TDD) — процесс разработки программного обеспечения, посредством которого мы параллельно улучшаем наши тестовые примеры и программный код. TDD помогает увеличить покрытие кода модульными тестами.
Процесс TDD выглядит следующим образом:
- Разработка модульных тестов для подмножества большого проекта
- Быстрое написание кода вышеупомянутого подмножества
- В случае каких-либо ошибок, рефакторинг вашего кода. В противном случае повторите цикл TDD.
- Продолжайте, пока весь ваш проект не будет завершен без явных ошибок.
Следующий рисунок иллюстрирует этот процесс:
Цикл разработки через тестирование
7. Один вариант использования на модульный тест
Каждый тест должен фокусироваться на одном варианте использования и проверять, что результат соответствует ожидаемому для этого тестируемого метода. Сосредоточив внимание на одном варианте использования, вы получите более четкое представление о корне проблемы в случае, если тест не пройден (в отличие от тестирования нескольких вариантов использования).
8. Избегайте логики в тестах
Чтобы уменьшить вероятность ошибок, ваш тестовый код должен практически не содержать логических условий или ручных конкатенаций строк.
9. Сокращение тестовых зависимостей
Тесты не должны зависеть друг от друга. Уменьшая зависимости между модулями, исполнители тестов могут одновременно запускать тесты для разных фрагментов кода. Модуль может считаться пригодным для тестирования только в том случае, если его зависимости размещены (т. е. заглушки) в тестовом коде. Никакие реальные или внешние зависимости не должны влиять на результат теста.
10. Стремитесь к максимальному охвату тестами
Хотя мы можем стремиться к 100% охвату тестами, это может быть не всегда желательно или возможно. Такое всестороннее тестирование может иметь финансовые и временные требования, превышающие наши возможности. В некоторых случаях такое комплексное тестирование теоретически невозможно (т.е. неразрешимо). При этом мы должны стремиться к максимально возможному охвату с учетом наших ограничений.
11. Храните надлежащую тестовую документацию
Ведение тестовой документации поможет как разработчикам, так и, в некоторых случаях, конечным пользователям (например, в случае с API).
Тестовая документация должна соответствовать следующим критериям:
- Рецензируемый: тест любого данного ресурса может быть рецензирован другими.
- Повторяемость: тест документируется таким образом, что его можно повторять несколько раз. Это позволяет нам убедиться, что ошибка исправлена в обновленном фрагменте кода, повторив тот же тест.
- Возможность архивирования: тесты и связанные с ними ошибки могут быть заархивированы в документации, которая служит ценным ресурсом для будущих расширений проекта.
Unit-тестирование — это разновидность тестирования в программной разработке, которое заключается в проверке работоспособности отдельных функциональных модулей, процессов или частей кода приложения. Unit-тестирование позволяет избежать ошибок или быстро исправить их при обновлении или дополнении ПО новыми компонентами, не тратя время на проверку программного обеспечения целиком.
Основной смысл модульного тестирования заключается в том, чтобы избежать накапливания ошибок в будущем, а также исключить регрессию уже отлаженных модулей. Например, у вас есть в целом готовое приложение, к которому необходимо добавить несколько новых функций или процессов. Если сначала выполнить интеграцию компонентов, а потом протестировать полностью «собранное» ПО, то ошибки в дополнениях могут привести к нестабильной работе всего приложения. Чтобы этого не произошло, легче протестировать добавляемые функции изолированно, а после устранения всех багов интегрировать их в программу.
Таким образом, unit-тестирование решает следующие задачи:
- поиск и исправление ошибок на ранних стадиях разработки программного продукта и, следовательно, снижение затрат в дальнейшем;
- лучшее понимание разработчиками базового кода проекта, более простая и быстрая корректировка продукта;
- повторное использование кода, в том числе с переносом (вместе с тестами) в другие продукты;
- использование юнит-тестов как проектной документации, по которой разработчики, не знакомые с кодом, могут понять принцип его работы.
Применять модульное тестирование при разработке программных продуктов рекомендуется по следующим причинам:
- Простота. Написать тест для отдельного модуля проще, чем для приложения в целом. Соответственно, если нужно проверить не всю программу, а лишь ее часть (например, вышедшее обновление или патч), то можно использовать модульное тестирование, предварительно изолировав проверяемый фрагмент кода. Хотя интеграционное тестирование нужно будет провести в любом случае.
- Информативность. Хорошо составленный тест помогает разработчикам понять API приложения, функционал модуля, особенности его использования. Особенно это полезно в том случае, если при работе над проектом произошла смена ответственных за разработку и проверку специалистов.
- Параллельная разработка. Модульное тестирование позволяет проверить работу одного компонента приложения независимо от других. Благодаря этому можно параллельно разрабатывать различные программные модули, тем самым сократив время на создание и отладку продукта.
- Возможность повторного использования. Создав однажды тест для проверки отдельного модуля, разработчик может вернуться к нему позднее, чтобы протестировать работу компонента еще раз. Регрессионное тестирование состоит в написании контрольных примеров для всех функций, которые помогают выявить ошибки, вызванные внесенными изменениями.
Несмотря на свои достоинства, модульное тестирование не является панацеей от всех болезней кода:
- Модульное тестирование не гарантирует, что будут найдены все ошибки. Причина в том, что даже в относительно простых программах невозможно предугадать все сценарии их выполнения.
- Unit-тестирование применяется к изолированным фрагментам кода, поэтому может выявить только ошибки проверяемого модуля. Оно не способно показать баги, возникающие при интеграции модуля с другими компонентами приложения. Также unit-тестирование не способно выявить системные ошибки продукта в целом.
Часто unit-тестирование путают с интеграционным, но это два разных по реализации и назначению уровня проверки программного обеспечения. Отличительные особенности модульного тестирования:
- узкая специализация — проверке подвергаются отдельные модули, а не все приложение в целом;
- простая реализация — тестирование модулей по отдельности (особенно при параллельной разработке) достаточно легкое в плане реализации, может проводиться без привлечения внешних ресурсов.
Напротив, интеграционное тестирование отличается следующими особенностями:
- общей направленностью — проверке подвергается не каждый модуль, а вся система, включая основное ядро и функциональные компоненты;
- сложностью — интеграционное тестирование проводится в среде, максимально близкой к реальной, поэтому требует привлечения внешних ресурсов (баз данных, веб-серверов).
В реальной практике эти два уровня тестирования не противопоставляются, а дополняют друг друга. Проверка каждого модуля снижает количество багов, которые обязательно проявятся при интеграции компонентов. А интеграционное тестирование позволит оценить взаимодействие программных модулей друг с другом и ядром приложения.
Ручное. Проводится максимально просто по заранее составленному документу с пошаговыми инструкциями. Однако такой подход возможен только с небольшими и несложными фрагментами кода и к тому же даже в этом случае он занимает много времени.
Автоматизированное. Unit-тестирование заключается в использовании специально разработанных тестовых сред, которые проверяют работу модуля и выявляют в ней ошибки. Такой подход имеет следующие особенности:
- Для каждой функциональной части приложения пишется отдельный модульный тест. Применять один и тот же тест для проверки разных компонентов нельзя.
- Проверяемый модуль должен быть изолирован от ядра приложения и других компонентов, чтобы исключить искажение результатов тестирования. Поэтому модульная проверка проводится не в естественной среде, а в специально разработанной тестовой.
- Использование автоматизированной тестовой среды позволяет смоделировать различные сценарии поведения кода. Если по ходу проверки были выявлены серьезные ошибки, такая система останавливает процесс до их устранения разработчиком, а потом снова запускает тест.
«Черного ящика». В этом случае тестирование происходит по входным и выходным сигналам модуля без анализа структуры его кода. Чаще всего такой метод применяется, когда проверку выполняет разработчик, который не участвовал в создании компонента.
«Белого ящика». Суть этого метода в том, что тестируются внутренняя структура модуля, его возможности, особенности поведения, реакция на входные сигналы и т.д. Иными словами, компонент изначально полностью прозрачен и понятен разработчику, который оценивает все внутренние и внешние аспекты его работы.
Для понимания unit-тестирования рассмотрим подробнее, как оно происходит по методу «белого ящика». В этом случае оно состоит из трех этапов:
- Анализ отдельного модуля. На этой стадии тестирования разработчик изучает внутреннюю структуру кода, функционал и поведение исследуемого компонента. Данный этап пройдет значительно быстрее, если программист сам создавал модуль или участвовал в его создании. Если нет — ему придется поднимать соответствующую документацию, консультироваться с создателем тестируемого фрагмента кода. Главная задача заключается в полном понимании того, как устроен и работает проверяемый программный компонент.
- Создание кейс-теста. Это сценарий или модель, которые должны показать, как проверяемый модуль ведет себя в реальной обстановке. Кейс-тесты создают искусственную среду, максимально близкую к реальной, но без привлечения внешних ресурсов, которые обычно задействуются в работе программного обеспечения (веб-серверов, баз данных и т.д.).
- Тестирование модуля. Проверяемый компонент, предварительно изолированный от ядра приложения и других модулей, запускается в кейс-тесте. При этом разработчик смотрит на то, как он реагирует на входные сигналы, как работает сам код, соответствует ли его структура выполняемым задачам, анализирует возможные ошибки и т.д.
Часто к одному и тому же компоненту ПО разработчик применяет различные методики тестирования. Указанные методы «черного и белого ящиков» не исчерпывают всех методик и инструментов проверки. Зачастую разработчик создает под каждый проект уникальные способы тестирования, учитывающие особенности программного продукта.
Стандартна ситуация, когда разработчик сначала написал код, а затем создает под него тест и выполняет проверку. Но в программировании часто используется и обратный процесс: сначала разрабатывается тест, а модуль создается на его основе. Такой подход называется «разработка через тестирование». Суть его в том, чтобы с помощью заранее написанного теста определить требования к будущему программному компоненту. Цикл разработки через тестирование насчитывает несколько этапов:
- Добавление теста. Оно происходит перед добавлением каждой новой функции в программу. Написанный тест не запускается по причине того, что проверяемый фрагмент кода еще не написан. Если тестирование сработало — значит, аналогичная или похожая функция в программе уже есть или тест написан некорректно. Сам тест тоже представляет собой программу, поэтому разработчик предварительно должен четко понять, какие результаты она должна показать в случае успешного тестирования.
- Написание кода. Ориентируясь на то, как должна себя повести тест-программа в «идеальном» случае, разработчик пишет код самого модуля. Причем он не обязан быть сразу совершенным — все неточности будут отшлифованы в последующих циклах разработки. Главное, что требуется от кода, — это прохождение теста. Как только разрабатываемый фрагмент написан, он прогоняется через тест-программу и анализируется.
- Рефакторинг. Убедившись, что написанный модуль успешно проходит тест, разработчик проверяет его на дублирование, неточности, мусорный код и т.д. Задача на этом этапе — максимально очистить фрагмент, сделать его более прозрачным, простым и понятным.
Разработка через тестирование не ограничивается одним циклом: они повторяются каждый раз при добавлении в приложение новых функций, процессов или других объектов. Если в очередной итерации ранее проходивший тестирование код вдруг выдал ошибку, разработчик всегда может откатить внесенные изменения, которые ее вызвали.
Этот метод разработки имеет свои преимущества:
- Код становится более простым и понятным, так как пишется под конкретные требования, заданные в тесте.
- Сокращается время разработки, в том числе за счет более частого использования отката модуля к работающей версии, чем отладки неработающей.
- Дизайн программы становится более удобным для пользователей, так как продумывается заранее, до написания кода, а не подгоняется под него.
- Снижается количество багов, так как разработчик изначально знает, что хочет получить от своего кода, а не использует метод проб и ошибок.
- Заранее написанный тест можно использовать в дальнейшем в качестве проектной документации к программному продукту.
Чтобы модульное тестирование было максимально эффективным, тесты должны:
- соответствовать конкретному модулю — нельзя применять один и тот же тест для тестирования разных по назначению и реализации программных компонентов;
- быть автоматизированными — тест лучше вписать в сам код, тогда он будет запускаться автоматически и сильно упростит жизнь разработчику;
- быть своевременными — если тест нельзя написать до разработки самого кода, его лучше создавать параллельно, что сэкономит много времени в дальнейшем;
- отвечать основным задачам — при написании теста не нужно стараться учесть все возможные сценарии, лучше сосредоточиться сначала на основных, а остальные дополнять по мере необходимости;
- иметь хорошее название — описывающее, что именно тестируется, в каких условиях и с каким желаемым результатом.
Модульное тестирование — не универсальный инструмент проверки программного продукта. В некоторых ситуациях оно лишь отнимет время и силы, не показав значимого результата, например:
- при тестировании сложных и разветвленных алгоритмов, таких как красно-черное дерево, придется разработать большое число тестов, что существенно усложнит и замедлит проверку;
- отсутствии четких результатов — например, в математическом моделировании природных процессов, настолько сложных, что их «выход» невозможно спрогнозировать, а можно только описать в виде интервалов вероятных значений;
- тестировании кода, взаимодействующего с системой, — например, модуля, связанного с портами, таймерами и другими «нестабильными» компонентами, от которых его сложно изолировать;
- проверке всего приложения — модульное тестирование не покажет ошибки интеграции, баги ядра и другие аспекты, не относящиеся непосредственно к конкретному модулю;
- недостаточной квалификации самого разработчика и низкой культуре программирования, так как модульное тестирование работает только при строгом соблюдении технологии, постоянном отслеживании всех вносимых в модуль изменениях.
Unit-тестирование окажется бесполезным и при проверке максимально простого кода. Точнее, оно сработает и покажет правильный результат, но сил на написание теста уйдет больше, чем на «ручной» анализ модуля.
Unit-тестирование — это эффективный и полезный инструмент, позволяющий избежать накопления ошибок при разработке программного обеспечения и сильно упрощающий проверку на более высоких уровнях (интеграционную, системную, приемочную).
Переход к большим
программам требует специальных способов
структурирования процесса тестирования.
Тестирование
модулей (или блоков) представляет собой
процесс тестирования отдельных
подпрограмм или процедур программы.
Здесь подразумевается, что прежде чем
начинать тестирование программы в
целом, следует протестировать отдельные
небольшие модули, образующие эту
программу. Такой подход мотивируется
тремя причинами. Во-первых, появляется
возможность активно управлять процессом
тестирования, поскольку внимание
первоначально концентрируется на
небольших модулях программы. Во-вторых,
облегчается задача отладки программы,
т.е. обнаружения места ошибки и исправление
текста программы. Наконец, в-третьих,
допускается параллелизм, что позволяет
одновременно тестировать несколько
модулей.
Цель тестирования
модулей — сравнение функций, реализуемых
модулем, со спецификациями его функций
или интерфейса. Проблема состоит не в
том, чтобы установить соответствие
модуля его спецификации, а в том, чтобы
показать противоречие между ними.
Тестирование
модулей в основном ориентировано на
принцип белого ящика. Процедура создания
набора тестов для тестирования модулей
такова: анализируется логика отдельного
модуля с помощью одного или нескольких
методов белого ящика, а затем этот набор
тестов применяется при тестировании
методами черного ящика по спецификации
модуля.
Реализация процесса
тестирования модулей опирается на два
ключевых положения: построение
эффективного набора тестов и выбор
способа, посредством которого модули
комбинируются при построении из них
рабочей программы. Второе условие
является весьма важным, т.к. оно задает
форму написания тестов модуля, типы
средств, используемых при тестировании,
порядок кодирования и тестирования
модулей, стоимость генерации тестов и
стоимость отладки.
Вообще говоря,
процесс тестирования и отладки тесно
связан с процессом проектирования
системы. Порой ошибки, обнаруживаемые
в отдельных модулях, бывают настолько
фатальными, что приходится перепроектировать
почти всю программу. Поэтому рассматривая
в данном разделе методику тестирования
и отладки программы “МРК ”, мы остановимся
также и на некоторых аспектах его
проектирования.
Очевидно, что
первоначально целесообразно тестировать
и отлаживать каждый компонент системы
в отдельности, а затем переходить к
испытаниям всего программного комплекса
в целом.
Существует два
подхода к комбинированию модулей:
пошаговое и монолитное тестирование.
В пошаговом тестировании в свою очередь
существуют два способа: тестирование
снизу вверх (восходящее) и тестирование
сверху вниз (нисходящее).
Возникает вопрос:
что лучше — выполнить по отдельности
тестирование каждого модуля, а затем
комбинируя их, сформировать рабочую
программу или же каждый модуль для
тестирования подключать к набору ранее
оттестированных модулей?
Первый подход
обычно называют монолитным методом
тестирования, или методом «большого
удара» при тестировании и сборке
программы; второй подход известен как
пошаговый метод тестирования или сборки.
Метод пошагового
тестирования предполагает, что модули
тестируются не изолированно друг от
друга, а подключаются поочередно для
выполнения теста к набору уже ранее
оттестированных модулей.
Монолитное
тестирование выгодно на начальном этапе
тестирования, когда нет готового костяка
программы и еще нельзя воспользоваться
пошаговым методом. В этом случае
монолитное тестирование очень похоже
на пошаговое, в котором вместо
оттестированных модулей одни заглушки.
Пошаговое
тестирование имеет целый ряд преимуществ
перед монолитным тестированием на
среднем окончательном этапе тестирования:
1. Монолитное
тестирование требует больших затрат
труда, т.к. в этом случае для каждого
тестируемого модуля программы требуется
написать множество модулей-заглушек.
В случае же пошагового тестирования
вместо модулей-заглушек используются
ранее оттестированные модули.
2. При пошаговом
тестировании раньше обнаруживаются
ошибки в интерфейсах между модулями,
поскольку раньше начинается сборка
программы. В противоположность этому
при монолитном тестировании модули «не
видят друг друга» до последней фазы
процесса тестирования.
3. Отладка программ
при пошаговом тестировании легче. Если
есть ошибки в межмодульных интерфейсах,
то при монолитном тестировании они
могут быть обнаружены лишь тогда, когда
собрана вся программа. В этот момент
локализовать ошибку довольно трудно,
поскольку она может находиться в любом
месте программы. Напротив, при пошаговом
тестировании ошибки такого типа в
основном связаны с тем модулем, который
подключается последним.
4. При монолитном
тестировании модуля результаты ограничены
только этим модулем. В случае пошагового
тестирования модули, оттестированные
ранее, затем используются в качестве
заглушек для тестирования других
модулей, и таким образом, подвергаются
дополнительной проверке.
Убедившись в
преимуществах пошагового тестирования
перед монолитным, исследуем две возможные
стратегии тестирования : нисходящее и
восходящее тестирование.
Нисходящее
тестирование.
Нисходящее
тестирование начинается с верхнего
головного модуля программы. Строгой,
корректной процедуры подключения
очередного последовательно тестируемого
модуля не существует. Единственное
правило, которым следует руководствоваться
при выборе очередного модуля, состоит
в том, что им должен быть один из модулей,
вызываемых модулем, предварительно
прошедшим тестирование.
При выборе
последовательности тестирования модулей
рекомендуется придерживаться двух
основных правил:
1. Если в программе
есть критические в каком либо смысле
части, то целесообразно выбирать
последовательность, которая включала
бы эти части как можно раньше. Критическими
могут быть сложный модуль, модуль с
новым алгоритмом или модуль со значительным
числом предполагаемых ошибок.
2. Модули, включающие
операции ввода-вывода, также необходимо
включать в последовательность тестирования
как можно раньше.
Основным
преимуществом нисходящего тестирования
является то, что уже на ранней стадии
имеется рабочая версия программы,
выполняющая реальные операции
ввода-вывода, в то время как часть
внутренних функций имитируется
заглушками. Эта рабочая версия позволяет
выявить ошибки и проблемы, связанные с
организацией взаимодействия с человеком;
она дает возможность продемонстрировать
программу пользователю.
В нашем случае
когда окончательная версия программы
предполагается не очень большой,
состоящей из небольшого количества
модулей такая стратегия подходит нам
лучше других.
Восходящее
тестирование
Данная стратегия
предполагает начало тестирования с
терминальных модулей (т.е. модулей, не
вызывающих другие модули). Как и ранее,
здесь нет такой процедуры для выбора
модуля, тестируемого на следующем шаге,
которой бы отдавалось предпочтение.
Единственное правило состоит в том,
чтобы очередной модуль вызывал уже
оттестированные модули. Как и при
нисходящем тестировании на последовательность
тестирования влияет критичность модуля.
Такой подход мы
применим на самом начальном отладочном
этапе, когда стоит вопрос об исправности
самых простых модулей программы, которые
будут вызываться модулями, проходящими
монолитное тестирование.
Соседние файлы в папке SPARK
- #
- #
- #
- #
- #
- #
- #
Классификация ошибок
Отладка – это процесс локализации и исправления ошибок, обнаруженных при тестировании программного обеспечения. Локализацией называют процесс определения оператора программы, выполнение которого вызвало нарушение нормального вычислительного процесса. Доя исправления ошибки необходимо определить ее причину, т. е. определить оператор или фрагмент, содержащие ошибку. Причины ошибок могут быть как очевидны, так и очень глубоко скрыты.
В целом сложность отладки обусловлена следующими причинами:
- требует от программиста глубоких знаний специфики управления используемыми техническими средствами, операционной системы, среды и языка программирования, реализуемых процессов, природы и специфики различных ошибок, методик отладки и соответствующих программных средств;
- психологически дискомфортна, так как необходимо искать собственные ошибки и, как правило, в условиях ограниченного времени;
- возможно взаимовлияние ошибок в разных частях программы, например, за счет затирания области памяти одного модуля другим из-за ошибок адресации;
- отсутствуют четко сформулированные методики отладки.
В соответствии с этапом обработки, на котором проявляются ошибки, различают:
- синтаксические ошибки — ошибки, фиксируемые компилятором (транслятором, интерпретатором) при выполнении синтаксического и частично семантического анализа программы;
- логические ошибки — …;
- ошибки компоновки — ошибки, обнаруженные компоновщиком (редактором связей) при объединении модулей программы;
- ошибки выполнения — ошибки, обнаруженные операционной системой, аппаратными средствами или пользователем при выполнении программы.
Синтаксические ошибки. Синтаксические ошибки относят к группе самых простых, так как синтаксис языка, как правило, строго формализован, и ошибки сопровождаются развернутым комментарием с указанием ее местоположения. Определение причин таких ошибок, как правило, труда не составляет, и даже при нечетком знании правил языка за несколько прогонов удается удалить все ошибки данного типа.
Следует иметь в виду, что чем лучше формализованы правила синтаксиса языка, тем больше ошибок из общего количества может обнаружить компилятор и, соответственно, меньше ошибок будет обнаруживаться на следующих этапах. В связи с этим говорят о языках программирования с защищенным синтаксисом и с незащищенным синтаксисом. К первым, безусловно, можно отнести Pascal, имеющий очень простой и четко определенный синтаксис, хорошо проверяемый при компиляции программы, ко вторым — Си со всеми его модификациями. Чего стоит хотя бы возможность выполнения присваивания в условном операторе в Си, например:
if (c = n) x = 0; /* в данном случае не проверятся равенство с и n, а выполняется присваивание с значения n, после чего результат операции сравнивается с нулем, если программист хотел выполнить не присваивание, а сравнение, то эта ошибка будет обнаружена только на этапе выполнения при получении результатов, отличающихся от ожидаемых.
Ошибки компоновки. Ошибки компоновки, как следует из названия, связаны с проблемами, обнаруженными при разрешении внешних ссылок. Например, предусмотрено обращение к подпрограмме другого модуля, а при объединении модулей данная подпрограмма не найдена или не стыкуются списки параметров. В большинстве случаев ошибки такого рода также удается быстро локализовать и устранить.
Ошибки выполнения. К самой непредсказуемой группе относятся ошибки выполнения. Прежде всего они могут иметь разную природу, и соответственно по-разному проявляться. Часть ошибок обнаруживается и документируется операционной системой. Выделяют четыре способа проявления таких ошибок:
- появление сообщения об ошибке, зафиксированной схемами контроля выполнения машинных команд, например, переполнении разрядной сетки, ситуации «деление на ноль», нарушении адресации и т. п.;
- появление сообщения об ошибке, обнаруженной операционной системой, например, нарушении защиты памяти, попытке записи на устройства, защищенные от записи, отсутствии файла с заданным именем и т. п.;
- «зависание» компьютера, как простое, когда удается завершить программу без перезагрузки операционной системы, так и «тяжелое», когда для продолжения работы необходима перезагрузка;
- несовпадение полученных результатов с ожидаемыми.
Примечание. Отметим, что, если ошибки этапа выполнения обнаруживает пользователь, то в двух первых случаях, получив соответствующее сообщение, пользователь в зависимости от своего характера, степени необходимости и опыта работы за компьютером, либо попробует понять, что произошло, ища свою вину, либо обратится за помощью, либо постарается никогда больше не иметь дела с этим продуктом. При «зависании» компьютера пользователь может даже не сразу понять, что происходит что-то не то, хотя его печальный опыт и заставляет волноваться каждый раз, когда компьютер не выдает быстрой реакции на введенную команду, что также целесообразно иметь в виду. Также опасны могут быть ситуации, при которых пользователь получает неправильные результаты и использует их в своей работе.
Причины ошибок выполнения очень разнообразны, а потому и локализация может оказаться крайне сложной. Все возможные причины ошибок можно разделить на следующие группы:
- неверное определение исходных данных,
- логические ошибки,
- накопление погрешностей результатов вычислений.
Методы отладки программного обеспечения
Отладка программы в любом случае предполагает обдумывание и логическое осмысление всей имеющейся информации об ошибке. Большинство ошибок можно обнаружить по косвенным признакам посредством тщательного анализа текстов программ и результатов тестирования без получения дополнительной информации. При этом используют различные методы:
- ручного тестирования;
- индукции;
- дедукции;
- обратного прослеживания.
Метод ручного тестирования
Это — самый простой и естественный способ данной группы. При обнаружении ошибки необходимо выполнить тестируемую программу вручную, используя тестовый набор, при работе с которыми была обнаружена ошибка. Метод очень эффективен, но не применим для больших программ, программ со сложными вычислениями и в тех случаях, когда ошибка связана с неверным представлением программиста о выполнении некоторых операций. Данный метод часто используют как составную часть других методов отладки.
Метод индукции
Метод основан на тщательном анализе симптомов ошибки, которые могут проявляться как неверные результаты вычислений или как сообщение об ошибке. Если компьютер просто «зависает», то фрагмент проявления ошибки вычисляют, исходя из последних полученных результатов и действий пользователя. Полученную таким образом информацию организуют и тщательно изучают, просматривая соответствующий фрагмент программы. В результате этих действий выдвигают гипотезы об ошибках, каждую из которых проверяют. Если гипотеза верна, то детализируют информацию об ошибке, иначе — выдвигают другую гипотезу. Последовательность выполнения отладки методом индукции показана на рисунке в виде схемы алгоритма.
Самый ответственный этап — выявление симптомов ошибки. Организуя данные об ошибке, целесообразно записать все, что известно о её проявлениях, причем фиксируют, как ситуации, в которых фрагмент с ошибкой выполняется нормально, так и ситуации, в которых ошибка проявляется. Если в результате изучения данных никаких гипотез не появляется, то необходима дополнительная информация об ошибке. Дополнительную информацию можно получить, например, в результате выполнения схожих тестов. В процессе доказательства пытаются выяснить, все ли проявления ошибки объясняет данная гипотеза, если не все, то либо гипотеза не верна, либо ошибок несколько.
Метод дедукции
По методу дедукции вначале формируют множество причин, которые могли бы вызвать данное проявление ошибки. Затем анализируя причины, исключают те, которые противоречат имеющимся данным. Если все причины исключены, то следует выполнить дополнительное тестирование исследуемого фрагмента. В противном случае наиболее вероятную гипотезу пытаются доказать. Если гипотеза объясняет полученные признаки ошибки, то ошибка найдена, иначе — проверяют следующую причину.
Метод обратного прослеживания
Для небольших программ эффективно применение метода обратного прослеживания. Начинают с точки вывода неправильного результата. Для этой точки строится гипотеза о значениях основных переменных, которые могли бы привести к получению имеющегося результата. Далее, исходя из этой гипотезы, делают предложения о значениях переменных в предыдущей точке. Процесс продолжают, пока не обнаружат причину ошибки.
Методы и средства получения дополнительной информации
Для получения дополнительной информации об ошибке можно выполнить добавочные тесты или использовать специальные методы и средства:
- отладочный вывод;
- интегрированные средства отладки;
- независимые отладчики.
Отладочный вывод. Метод требует включения в программу дополнительного отладочного вывода в узловых точках. Узловыми считают точки алгоритма, в которых основные переменные программы меняют свои значения. Например, отладочный вывод следует предусмотреть до и после завершения цикла изменения некоторого массива значений. (Если отладочный вывод предусмотреть в цикле, то будет выведено слишком много значений, в которых, как правило, сложно разбираться.) При этом предполагается, что, выполнив анализ выведенных значений, программист уточнит момент, когда были получены неправильные значения, и сможет сделать вывод о причине ошибки.
Данный метод не очень эффективен и в настоящее время практически не используется, так как в сложных случаях в процессе отладки может потребоваться вывод большого количества — «трассы» значений многих переменных, которые выводятся при каждом изменении. Кроме того, внесение в программы дополнительных операторов может привести к изменению проявления ошибки, что нежелательно, хотя и позволяет сделать определенный вывод о ее природе.
Примечание. Ошибки, исчезающие при включении в программу или удалению из нее каких-либо «безобидных» операторов, как правило, связаны с «затиранием» памяти. В результате добавления или удаления операторов область затирания может сместиться в другое место и ошибка либо перестанет проявляться, либо будет проявляться по-другому.
Интегрированные средства отладки. Большинство современных сред программирования (Delphi, Builder C++, Visual Studio и т. д.) включают средства отладки, которые обеспечивают максимально эффективную отладку. Они позволяют:
- выполнять программу по шагам, причем как с заходом в подпрограммы, так и выполняя их целиком;
- предусматривать точки останова;
- выполнять программу до оператора, указанного курсором;
- отображать содержимое любых переменных при пошаговом выполнении;
- отслеживать поток сообщений и т. п.
Отладка с использованием независимых отладчиков.
При отладке программ иногда используют специальные программы — отладчики, которые позволяют выполнить любой фрагмент программы в пошаговом режиме и проверить содержимое интересующих программиста переменных. Как правило такие отладчики позволяют отлаживать программу только в машинных командах, представленных в 16-ричном коде.
Общая методика отладки программного обеспечения
Суммируя все сказанное выше, можно предложить следующую методику отладки программного обеспечения:
1 этап — изучение проявления ошибки — если выдано какое-либо сообщение или выданы неправильные или неполные результаты, то необходимо их изучить и попытаться понять, какая ошибка могла так проявиться. При этом используют индуктивные и дедуктивные методы отладки. В результате выдвигают версии о характере ошибки, которые необходимо проверить. Для этого можно применить методы и средства получения дополнительной информации об ошибке. Если ошибка не найдена или система просто «зависла», переходят ко второму этапу.
2 этап — локализация ошибки — определение конкретного фрагмента, при выполнении которого произошло отклонение от предполагаемого вычислительного процесса. Локализация может выполняться:
- путем отсечения частей программы, причем, если при отсечении некоторой части программы ошибка пропадает, то это может означать как то, что ошибка связана с этой частью, так и то, что внесенное изменение изменило проявление ошибки;
- с использованием отладочных средств, позволяющих выполнить интересующих нас фрагмент программы в пошаговом режиме и получить дополнительную информацию о месте проявления и характере ошибки, например, уточнить содержимое указанных переменных.
При этом если были получены неправильные результаты, то в пошаговом режиме проверяют ключевые точки процесса формирования данного результата. Как подчеркивалось выше, ошибка не обязательно допущена в том месте, где она проявилась. Если в конкретном случае это так, то переходят к следующему этапу.
3 этап — определение причины ошибки — изучение результатов второго этапа и формирование версий возможных причин ошибки. Эти версии необходимо проверить, возможно, используя отладочные средства для просмотра последовательности операторов или значений переменных.
4 этап — исправление ошибки — внесение соответствующих изменений во все операторы, совместное выполнение которых привело к ошибке.
5 этап — повторное тестирование — повторение всех тестов с начала, так как при исправлении обнаруженных ошибок часто вносят в программу новые.
Следует иметь в виду, что процесс отладки можно существенно упростить, если следовать основным рекомендациям структурного подхода к программированию:
- программу наращивать «сверху-вниз», от интерфейса к обрабатывающим подпрограммам, тестируя ее по ходу добавления подпрограмм;
- выводить пользователю вводимые им данные для контроля и проверять их на допустимость сразу после ввода;
- предусматривать вывод основных данных во всех узловых точках алгоритма (ветвлениях, вызовах подпрограмм).
Кроме того, следует более тщательно проверять фрагменты программного обеспечения, где уже были обнаружены ошибки, так как вероятность ошибок в этих местах по статистике выше. Это вызвано следующими причинами. Во-первых, ошибки чаще допускают в сложных местах или в тех случаях, если спецификации на реализуемые операции недостаточно проработаны. Во-вторых, ошибки могут быть результатом того, что программист устал, отвлекся или плохо себя чувствует. В-третьих, как уже упоминалось выше, ошибки часто появляются в результате исправления уже найденных ошибок.
Источник:
Модульное тестирование является одним из основных видов тестирования программного обеспечения. Как и в случае с тестированием программного обеспечения, модульное тестирование не может гарантировать отсутствие ошибок после развертывания приложения. Однако, тестируя наименьшие повторяющиеся фрагменты кода в ваших приложениях, модульное тестирование помогает выявлять ошибки в строительных блоках вашего проекта до того, как они повлияют на ваше интегрированное приложение.
Будучи важным процессом разработки программного обеспечения, модульное тестирование является ценным навыком, который должен знать разработчик. Если бы мы могли следовать передовым методам модульного тестирования, мы уже проводили бы модульное тестирование на этапе разработки. Однако это не реально для всех проектов разработки программного обеспечения. Таким образом, модульное тестирование может выполняться после того, как ваш производственный код написан, и это необходимо для обеспечения того, чтобы расширенный и рефакторинговый код оставался функциональным.
Сегодня мы рассмотрим обзор модульного тестирования и лучших практик модульного тестирования.
Содержание
- Обзор модульного тестирования
- Что такое модульное тестирование?
- Преимущества модульного тестирования
- Тестовое покрытие
- Среды модульного тестирования
- 9 лучших практик модульного тестирования
- 1. Напишите тесты для ряда сценариев
- 2. Напишите хорошие названия тестов
- 3. Настройте автоматические тесты
- 4. Пишите детерминированные тесты
- 5. Организовать, действовать и утверждать (ААА)
- 6. Пишите тесты до или во время разработки
- 7. Один вариант использования на модульный тест
- 8. Избегайте логики в тестах
- 9. Сокращение тестовых зависимостей
- 10. Стремитесь к максимальному охвату тестами
- 11. Храните надлежащую тестовую документацию
Обзор модульного тестирования
Что такое модульное тестирование?
Модульное тестирование гарантирует, что модули в вашей программе работают должным образом. Поскольку человек, написавший фрагмент кода, лучше всего понимает его ожидаемое поведение, ответственность за модульное тестирование обычно лежит на разработчике. В сочетании со сквозными и интеграционными тестами модульное тестирование помогает обеспечить качество кода на ранних этапах процесса разработки.
Модуль — это наименьший фрагмент кода в вашей программе, который повторяем, тестируем и функционален. Единицами могут быть функции, классы, методы и так далее.
Уровни тестирования программного обеспечения
На предыдущем рисунке показано модульное тестирование наряду с другими уровнями тестирования программного обеспечения, где каждый уровень имеет свою область применения:
- Модульное тестирование проверяет каждый программный модуль.
- Интеграционное тестирование проверяет объединенные модули в соответствии с проектной документацией системы и функциональными требованиями.
- Функциональное тестирование проверяет желаемую функциональность программы на основе документов анализа требований и руководства пользователя (т. е. обеспечение ожидаемого результата для заданных входных данных). Это иногда делится на системное тестирование и приемочное тестирование.
Тестирование «черного ящика» и «белого ящика» — это два подхода к тестированию программного обеспечения, в которых:
- Тестирование черного ящика проверяет поведение программного обеспечения, не зная деталей реализации программного модуля. Эти тесты получены из документа спецификации программного обеспечения.
- Тестирование методом белого ящика проверяет фактическую реализацию программного модуля, при этом внутренняя архитектура программы известна тестировщику. Эти тесты дают нам представление о реализации для разработки более комплексных тестов.
Для модульного тестирования наиболее подходящим вариантом обычно является тестирование белого ящика, особенно когда наши модули меньше и их код легче понять. С другой стороны, тестирование «черного ящика» — хороший вариант для более поздних стадий проекта, когда модули были интегрированы для создания сложного программного обеспечения.
Преимущества модульного тестирования
Некоторые преимущества модульного тестирования включают в себя:
- Упрощает отладку : тестируя функциональность небольших модулей, вы можете обнаруживать ошибки до того, как они повлияют на другие модули в интегрированном приложении.
- Поощряет более слабосвязанный код : намеренно уменьшая взаимозависимости между модулями, мы получаем более слабосвязанный код, что является передовой практикой кодирования.
- Быстрее, чем функциональное тестирование : поскольку модули очень маленькие, вы можете запустить несколько модульных тестов за секунды (при условии, что вы их автоматизировали).
- Минимизация регрессии кода : после рефакторинга или расширения кода вы можете повторно запустить все наборы тестов, чтобы убедиться, что ваш новый или обновленный код не нарушает существующие функции.
- Лучшее покрытие кода. Разбивая приложение на мельчайшие тестируемые компоненты, модульное тестирование помогает увеличить покрытие кода.
Тестовое покрытие
Тестовое покрытие — это метрика, определяющая, насколько тщательно мы провели модульное тестирование нашего программного обеспечения. Как правило, мы хотим максимально увеличить тестовое покрытие.
Тестовое покрытие можно дополнительно классифицировать следующим образом:
- Охват функций: процент функций, которые были протестированы.
- Покрытие операторов: процент утверждений, которые были протестированы. Иногда он включает в себя все операторы, которые были выполнены хотя бы один раз во время тестирования.
- Покрытие пути: процент проверенных путей. Программное обеспечение может иметь несколько путей (или ответвлений), выбранных им в зависимости от условий, таких как ввод данных пользователем, определение среды и другие события.
Среды модульного тестирования
Платформа модульного тестирования — это программное обеспечение, которое позволяет нам быстро создавать модульные тесты и автоматизировать их выполнение. В случае сбоя теста фреймворки могут сохранить результаты или выдать утверждение.
Существуют десятки фреймворков для модульного тестирования, доступных для различных языков программирования. Некоторые популярные среды модульного тестирования включают Cunit, Moq, Cucumber, Selenium, Embunit, Sass True, HtmlUnit и JUnit (одна из наиболее широко используемых сред с открытым исходным кодом для языка программирования Java).
Мы можем сэкономить много времени и ресурсов с помощью фреймворков модульного тестирования, которые включают следующие функции:
- Набор тестов: набор тестовых случаев, сгруппированных для выполнения тестов, что позволяет нам объединять несколько похожих модульных тестов.
- Test runner: компонент, который автоматизирует выполнение серии тестов и возвращает результат теста пользователю.
- Приспособление для тестирования: предопределенное состояние или фиксированная среда, в которой мы запускаем тесты, гарантируя, что наши тесты повторяются при нескольких запусках тестов.
9 лучших практик модульного тестирования
1. Напишите тесты для ряда сценариев
При написании тестового примера убедитесь, что вы рассматриваете все возможные сценарии. Другими словами, не просто напишите тест на счастливый путь. Подумайте и о других сценариях, таких как обработка ошибок.
2. Напишите хорошие названия тестов
Хорошее имя модульного теста должно явно отражать цель тестового примера. Следуйте согласованным соглашениям об именах и используйте сокращения только в том случае, если они понятны читателю. Написание хороших имен тестов повышает читабельность кода, что облегчит вам и другим возможность расширять этот код в будущем.
3. Настройте автоматические тесты
Выберите автоматизированное модульное тестирование с помощью среды модульного тестирования. Еще лучше автоматизировать тесты в конвейере непрерывной интеграции (CI/CD).
Альтернативой автоматизированному тестированию является ручное тестирование, при котором мы вручную выполняем тестовые случаи и собираем их результаты. Как вы понимаете, ручное тестирование небольших модулей невероятно утомительно. Он также менее надежен. Автоматические тесты, безусловно, помогут вам в этом.
4. Пишите детерминированные тесты
Ложноположительные и отрицательные результаты являются обычным явлением при тестировании программного обеспечения, и мы должны приложить все усилия, чтобы свести их к минимуму. Цель состоит в том, чтобы иметь согласованные выходные данные для тестов, чтобы проверить желаемую функцию. Поэтому модульные тесты должны быть детерминированными. Другими словами, пока код теста не изменяется, детерминированный тест должен иметь стабильное поведение при каждом запуске теста.
5. Организовать, действовать и утверждать (ААА)
Протокол AAA является рекомендуемым подходом для структурирования модульных тестов. В качестве передовой практики модульного тестирования это улучшает читабельность вашего теста, придавая ему логическую последовательность. ААА иногда называют протоколом «Дано/Когда/Тогда».
Вы можете использовать протокол AAA для структурирования модульных тестов, выполнив следующие шаги:
- Упорядочить: Упорядочить настройку и инициализацию для теста.
- Act: Действуйте на единицу для данного теста.
- Утвердить: Утвердить или проверить результат
Следующий код демонстрирует структуру AAA при тестировании функции абсолютного значения в Python:
def test_abs_for_a_negative_number(): # Arrange negative = -2 # Act answer = abs(negative) # Assert assert answer == 2
6. Пишите тесты до или во время разработки
В идеальном мире мы пишем тестовый код перед написанием рабочего кода. Это известно как разработка через тестирование (TDD) — процесс разработки программного обеспечения, посредством которого мы параллельно улучшаем наши тестовые примеры и программный код. TDD помогает увеличить покрытие кода модульными тестами.
Процесс TDD выглядит следующим образом:
- Разработка модульных тестов для подмножества большого проекта
- Быстрое написание кода вышеупомянутого подмножества
- В случае каких-либо ошибок, рефакторинг вашего кода. В противном случае повторите цикл TDD.
- Продолжайте, пока весь ваш проект не будет завершен без явных ошибок.
Следующий рисунок иллюстрирует этот процесс:
Цикл разработки через тестирование
7. Один вариант использования на модульный тест
Каждый тест должен фокусироваться на одном варианте использования и проверять, что результат соответствует ожидаемому для этого тестируемого метода. Сосредоточив внимание на одном варианте использования, вы получите более четкое представление о корне проблемы в случае, если тест не пройден (в отличие от тестирования нескольких вариантов использования).
8. Избегайте логики в тестах
Чтобы уменьшить вероятность ошибок, ваш тестовый код должен практически не содержать логических условий или ручных конкатенаций строк.
9. Сокращение тестовых зависимостей
Тесты не должны зависеть друг от друга. Уменьшая зависимости между модулями, исполнители тестов могут одновременно запускать тесты для разных фрагментов кода. Модуль может считаться пригодным для тестирования только в том случае, если его зависимости размещены (т. е. заглушки) в тестовом коде. Никакие реальные или внешние зависимости не должны влиять на результат теста.
10. Стремитесь к максимальному охвату тестами
Хотя мы можем стремиться к 100% охвату тестами, это может быть не всегда желательно или возможно. Такое всестороннее тестирование может иметь финансовые и временные требования, превышающие наши возможности. В некоторых случаях такое комплексное тестирование теоретически невозможно (т.е. неразрешимо). При этом мы должны стремиться к максимально возможному охвату с учетом наших ограничений.
11. Храните надлежащую тестовую документацию
Ведение тестовой документации поможет как разработчикам, так и, в некоторых случаях, конечным пользователям (например, в случае с API).
Тестовая документация должна соответствовать следующим критериям:
- Рецензируемый: тест любого данного ресурса может быть рецензирован другими.
- Повторяемость: тест документируется таким образом, что его можно повторять несколько раз. Это позволяет нам убедиться, что ошибка исправлена в обновленном фрагменте кода, повторив тот же тест.
- Возможность архивирования: тесты и связанные с ними ошибки могут быть заархивированы в документации, которая служит ценным ресурсом для будущих расширений проекта.












