Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
kernigan_paik.doc
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
2.91 Mб
Скачать

6.2. Систематическое тестирование

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

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

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

Тестируйте сначала простые блоки. Общий подход к тестированию по возрастающей применим и к отдельным деталям программы. В первую очередь тестированию подлежат самые простые и чаще всего исполня­ющиеся блоки; только после того, как вы удостоверитесь в их коррек­тности, можно двигаться дальше. Таким образом, на каждом этапе вы увеличиваете объем тестируемого кода, будучи при этом уверенными в работоспособности основных его частей. Простые тесты обнаружива­ют простые ошибки. Каждый тест выполняет свой минимум по поиску новой потенциальной проблемы. И несмотря на то что каждую новую ошибку выловить труднее, чем предыдущую, вовсе не факт, что ее будет труднее и исправить.

В этом параграфе мы поговорим о путях выбора эффективных тес­тов и о порядке их применения, а в двух следующих параграфах обсу­дим способы механизации процесса для наиболее эффективного тести­рования.

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

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

• поиск в пустом массиве;

• поиск в массиве с одним элементом — пробное значение:

- меньше чем элемент массива;

- равно элементу массива;

- больше чем элемент массива;

• поиск в массиве с двумя элементами — пробные значения:

- тестируем все пять возможных вариантов;

• проверяем поведение при дублировании элемента *- пробные зна­чения:

- меньше значения в массиве;

- равно значению в массиве;

- больше значения в массиве;

• поиск в массиве с тремя элементами (так же, как и с двумя);

• поиск в массиве с четырьмя элементами (так же, как с двумя и тремя).

Если функция пройдет эти тесты без ошибок, она, по всей видимости, находится в неплохой форме, однако ее можно тестировать и дальше.

Приведенный набор тестов достаточно мал, чтобы выполнять их все вручную, но лучше создать оснастку (test scaffold — подмости тестирования) для механизации процесса. С этой целью мы напишем простейшую программу (по сути, драйвер). Она будет считывать строки, содержащие ключ, по которому будет производиться поиск, и размер массива; после этого будет создан массив указанного размера, содержащий значения 1, 3, 5 и т. п.; результат поиска будет выводиться на экран.

/* bintest main: утилита для тестирования binsearch */

int main(void)

{

int i, key, nelem, arr[1000];

while (scanf("%d %d", &key, &nelem) != EOF) {

for (i = 0; i < nelem; i++)

arr[i] = 2*i + 1;

printf("%d\n", binsearch(key, arr, nelem));

}

return 0;

}

Простейшая программка, но утилиты для тестирования и не должны быть сложными (естественно, при желаний можно расширить возмож­ности этой утилиты), их единственная задача — избавить вас от моно­тонных ручных проверок.

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

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

• Для теста компилятора скомпилируйте и запустите тестовые фай­лы. Результаты

работы этих программ надо сравнить с заранее определенными значениями.

• Для теста вычислительной программы выберите случаи, которые позволят

проверить алгоритм со всех сторон, — как простые случаи, так и сложные. Где

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

Например, вывод программы, численно считающей интегралы, может быть

проверен на непре­рывность и на соответствие результату, полученному по

формуле.

• Для тестирования графической программы недостаточно удостове­риться, что она в

состоянии нарисовать ящик; вместо этого прочти­те этот ящик обратно с экрана и

проверьте, что его стороны находят­ся там, где требуется.

Если в программе выполняются какие-то обратимые действия, убеди­тесь, что вы можете обратить данные в исходное состояние. Шифровка и дешифровка во многих случаях обратимы; если вы что-то зашифрова­ли и не смогли расшифровать, значит, что-то тут не так. То же самое от­носится и к программам сжатия данных. Иногда существует несколько способов обратного преобразования — тогда необходимо проверить все комбинации.

Проверяйте свойства сохранности данных. Многие программы со­храняют некоторые свойства вводимых данных. Инструменты вроде wc (подсчитывает строки, слова и символы) и sum (вычисляет контрольную сумму) помогут удостовериться в том, что вывод имеет тот же размер, то же количество слов или те же байты в некотором порядке и т. п. Другие (программы проверяют файлы на идентичность (сmр) или перечисляют иx различия (diff). Эти программы (или сходные с ними) доступны в большинстве сред программирования, и пренебрегать ими не стоит.

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

#include <stdio.h>

#include <ctype.h>

#include <limits.h>

unsigned long count[UCHAR_MAX+1];

/* freq main: выводит частоты появления байтов */

int main(void)

{

int c;

while ((с = getcharO) != EOF)

cout[c]++;

for (c = 0; с <= UCHAR_MAX; с++)

if (count[c] != 0)

printf("%.2x %c %lu\n",

c, isprint(c) ? с : '-', count[c]);

return 0;

}

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

Сравните независимые реализации. Независимые реализации библио­тек или программ должны выдавать одни и те же результаты. Например, два компилятора должны из одного и того же текста создавать програм­мы, которые на одной и той же машине будут вести себя одинаково, — по крайней мере, в большинстве случаев.

Иногда искомый ответ можно получить двумя различными спосо­бами, а иногда — написать тривиальную версию программы, не заботясь о быстродействии и используемых ресурсах, для сравнения результатов. Если две программы независимо друг от друга выдают одни и те же отве­ты, с большой долей уверенности можно считать, что обе они работают корректно; если же ответы их разнятся, значит, по крайней мере, в одной из них есть ошибки.

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

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

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

Мы использовали комбинацию описанных выше методов для тестирования программы markov из главы 3, эти тесты будут подробно описаны в последнем разделе главы.

Упражнение 6-3

Опишите, как вы будете тестировать freq.

Упражнение 6-4

Спроектируйте и реализуйте версию freq, которая подсчитывала бы частоты для других типов данных — таких, как 32-битовые целые или числа с плавающей точкой. Сможете ли вы добиться того, чтобы программа элегантно обрабатывала данные различных типов?

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]