Лекция 7. Тестирование и отладка

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

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

Особенности процесса тестирования программы состоят в том, что:

- отсутствует эталон программы, которому должны соответствовать все результаты тестирования проверяемой программы;

- принципиально невозможно создать тестовый набор для исчерпывающей проверки;

- отсутствуют формализованные критерии качества программ и процесса тестирования;

- необходимо создавать тесты, которые находят ошибки, а не демонстрируют правильность работы программы;

- необходимо привлекать для тестирования сторонних специалистов;

- необходимо избегать невоспроизводимых тестов.

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

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

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

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

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

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

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

Такой метод наиболее удобен при испытании программ модульной структуры и при наличии квалифицированных программистов.

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

Один из вариантов этого подхода состоит в тестировании одной полной ветви программы. Первой проверяется (с использованием при необходимости заглушек) управляющая логика. Некоторые системы реализуются очередями, причем вначале, до полного тестирования программы, обрабатывается некоторое подмножество данных. Такой порядок реализации системы возможен, поскольку при разработке каждой программы нетрудно предусмотреть отдельные ветви для обработки различных данных. Это может оказаться весьма полезным, т. к. часто 10 % обрабатываемых типов данных могут на 90 % обеспечить потребности пользователей.

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

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

- совместное редактирование связей уже проверенных модулей — комплексное тестирование.

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

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

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

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

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

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

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

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

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

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