Лекция № 10
Тестирование и отладка программ
Тестирование – выполнение программы с целью обнаружения ошибок.
Дейкстра: "Никакое тестирование не может подтвердить правильность программы: в лучшем случае, оно может показать только ее ошибочность".
Отладка – локализация и исправление ошибок.
Виды программных ошибок Способы их обнаружения
1. Синтаксические Статический контроль и диагностика компилятором и компоновщиком
2. Ошибки выполнения, выявляемые Динамический контроль:
автоматически:
а) переполнение, потеря порядка, ... - аппаратурой процессора (Вопрос 1)
б) несоответствие типов - run-time системы программирования
в) зацикливание - операционной системой – по превы-
шению лимита времени задачи
3. Программа не соответствует специ- Целенаправленное тестирование
фикации
4. Спецификация не соответствует Испытания, бета-тестирование
требованиям – ошибка спецификации
Глубина контроля 1-го вида зависит и от языка, и от компилятора. Строгая типизация весьма полезна: DO 3 I = 1.3 – “точка стоимостью 800 млн $” (вместо запятой) – ошибка в Фортран-программе бортового вычислителя ракеты к Венере [1]. Вопрос 2.
Набор ошибок 2-го вида может быть расширен программистом: контроль можно программировать с помощью утверждений (asserts) проверок, вставляемых в код. Это полезно для проверки правдоподобности промежуточных результатов вычислений и допустимости значений фактических параметров подпрограмм.
Собственно процесс тестирования направлен на выявление ошибок 3 и 4 видов. Большинство программистов сами исправляют 99% своих текущих ошибок. Однако порядка одной ошибки на 100 строк кода обычно еще остается, когда программист сдает работу тестеру, утверждая, что ошибок в ней нет [2].
Тест – это набор контрольных входных данных совместно с ожидаемыми результатами. К входным данным здесь относятся не только конкретные значения ввода, но и события, их последовательность и временные параметры. Ожидаемые результаты берутся из спецификации программы, а на этапе приемо-сдаточных испытаний это – ожидания пользователей.
Ключевой вопрос – полнота тестирования: какое количество каких тестов гарантирует возможно более полную проверку программы ? Исчерпывающая проверка на всем множестве входных данных недостижима. Пример: программа, вычисляющая функцию двух переменных: Y = f (X, Z). Если X, Y, Z – real, то полное число тестов (232)2 = 264 ≈ 1031. Если на каждый тест тратить 1 мс, то 1031 мс = 800 млн лет (отсюда видно, что ошибка FDIV Pentium’а вполне простительна). Все траектории выполнения кода также невозможно воспроизвести. В [1] приведена программа из двадцати строк кода (цикл и несколько операторов IF), у которой 1017 возможных путей выполнения.
Следовательно:
В любой нетривиальной программе на любой стадии ее готовности содержатся необнаруженные ошибки
Продолжительность тестирования – технико-экономическая проблема: компромисс между временем и полнотой. Поэтому нужно возможно меньшее количество хороших тестов с желательными свойствами:
Детективность: тест должен с большой вероятностью обнаруживать возможные ошибки.
Покрывающая способность: один тест должен выявлять как можно больше ошибок.
Воспроизводимость: ошибка должна выявляться независимо от изменяющихся условий (например, от временных соотношений) – это трудно достижимо для время-зависимых программ, реультаты которых часто невоспроизводимы.
Это благие пожелания; для направленного выбора руководствуются критериями выбора тестов. Критерий должен показать, когда некоторое конечное множество тестов достаточно для проверки программы с некоторой полнотой.
Два вида критериев:
Функциональные – если тесты составляются исходя из спецификации программы (тестирование черного ящика). Проверяется правильность выполнения программой всех ее заданных функций. Именно этим критериям в основном и следуют при независимом тестировании.
Структурные – если тесты составляются исходя из текста программы (тестирование прозрачного ящика). Проверяется правильность работы при прохождении всех участков кода. Эту работу программисты выполняют постоянно в ходе разработки.
Вид критерия Что должно обеспечивать множество тестов
А. Функциональные
1. Тестирование классов вх. данных ! Содержать представителей всех вх или вых
2. Тестирование классов вых. данных ! классов и точки на границах классов
3. Тестирование функций Каждая функция внешнего интерфейса должна быть проверена >= 1раза
Б. Структурные
Тестирование команд Каждая команда (оператор) д.б. выполнена
>= 1раза
Критерий С1 – тестир. ветвей Каждая ветвь д.б. выполнена >= 1раза
Критерий С2 – тестир. путей Каждый путь в графе программы д.б.
выполнен >= 1раза (Вопрос 3)
На рис 10-1 а) видно отличие тестирования команд (достаточен один тест) от С1 (необходимы два теста как минимум). Рис 10-1 б) иллюстрирует отличие С1 (достаточно двух тестов, покрывающих пути 1, 4 или 2, 3) от С2 (необходимо 4 теста для всех четырех путей). С2 в принципе недостижим в реальных программах из-за их цикличности, поэтому ограничиваются тремя путями для каждого цикла: 0, 1 и N повторений цикла.
Идея назначения классов эквивалентности вх/вых данных для функционального тестирования основана на разумном предположении, что программа на всем классе ведет себя так же, как на его одном представителе. Классы назначаются исходя из семантики решаемой задачи. В таблице 1 дан пример тестирования классов выходных данных: минимальный набор тестов для программы нахождения вещественных корней квадратного уравнения ax2 + bx + c = 0 (Грюнбергер, 1968).
Рис.10-1. Траектории вычислений при структурном тестировании
Таблица 1
|
a |
b |
c |
Ожидаемый результат |
Что проверяется |
1 |
2 |
-5 |
2 |
x1=2, x2=0.5 |
Случай вещественных корней |
2 |
3 |
2 |
5 |
Сообщение |
Случай комплексных корней |
3 |
3 |
-12 |
0 |
x1=4, x2=0 |
Нулевой корень |
4 |
0 |
0 |
10 |
Сообщение |
Неразрешимое уравнение |
5 |
0 |
0 |
0 |
Сообщение |
Неразрешимое уравнение |
6 |
0 |
5 |
17 |
Сообщение |
Не квадратное уравнение (деление на 0) |
7 |
9 |
0 |
0 |
x1=x2=0 |
Корень из 0 |
Таким образом, для этой программы предлагается минимальный набор из 7 функциона-льных тестов, исходя из 7 классов выходных данных.
Пример, где назначаются классы входных данных – на рис. 10-2. Здесь классы- точечные множества; внутри области А они отмечены штриховкой; символом * отмечены представители классов - тестовые значения. На рис. 10-2 предложен минимальный набор из 18 тестов – по одному для каждого класса и границы –
стороны многоугольника, ограничивающего область А. В состав тестового набора следует включать значения, непосредственно примыкающие к граничным. Например, если допустимые входные значения – целые от 1 до 99, то для тестирования допусти-мых данных можно выбрать 1 и 99, а для недопустимых – 0 и 100. Если программа получает 8 входных данных, то нужно предусмотреть 3 теста: ввод 8, 7 и 9 данных.
Запись классов эквивалентности входных данных в текстовой форме является частным случаем плана тестирования, пример которого приведен на рис. 10-3. Заметим, что классы эквивалентности могут пересекаться, как в этом примере (классы 1.2.4.1 и 1.2.4.3) – это приводит к некоторой избыточности, но не страшно. Задание 4.
Рис 10-2. Классы входных данных для тестирования
1. Ввод числа
1.1 Допустимые варианты
1.1.1 Числа от 0 до 99
1.2 Недопустимые варианты
0
> 99
Отрицательные числа
Буквы и другие нечисловые символы
Буквы
Символы с ASCII-кодами, меньшими кода 0
Символы с ASCII-кодами, большими кода 9
2. Ввод первой буквы имени
2.1 Допустимые варианты
2.1.1 Первый символ является заглавной буквой
<и т.д.>
Рис 10-3. Классы входных данных: фрагмент плана тестирования
Различие между функциональным и структурным тестированием относительно. Для интерактивных и реального времени программ оно стирается: входные данные – различные последовательности действий пользователя или внешних событий – однозначно отображаются на различные траектории переключения состояний программы, т.е. пути в ней. Таких траекторий – необозримо много, поэтому приходится ограничиваться тестированием наиболее вероятных действий пользователя или последовательностей событий, имитируемых специальной тестовой программой. К ним добавляются случайные действия, например, нажатие клавиш в случайные моменты времени. Профессиональные тестеры составляют схемы меню – диаграммы состояний и переходов при вводе диалоговых команд – для контроля полноты прохождения типичных траекторий диалога.
Нагрузочные тесты проверяют работу программы при различных конфигурациях аппаратуры (особенно при минимальных) и при совместном выполнении в мультипро-граммной среде.