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

6.4. Тестовые оснастки

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

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

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

Чтобы проиллюстрировать повествования, мы создадим тест для memset, одной из функций семейства mem... стандартной библиотеки C/C++. Эти функции часто пишутся на языке ассемблера для конкретных машин, поскольку их быстродействие очень важно. Однако чем более тонко они настраиваются на конкретные условия, тем больше вероятность возникновения в них ошибок и тем более тщательно дол­жны они тестироваться.

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

Функция memset (s, с, n) записывает байт с в п байтов памяти, начи­ная с адреса s, и возвращает s. Если нет ограничений на скорость работы, написать такую функцию — не проблема:

/* memset: устанавливает первые n байтов s равными с */

void *meniset(void *s, int c, size_t n)

{

size_t i;

char *p;

p = (char *) s;

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

p[i] = c;

return s;

}

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

Тестирование базируется на комбинации всесторонних проверок (в частности, естественно, проверок граничных условий в потенци­ально опасных точках). Для memset граничными, очевидно, являются такие значения n, как ноль, один и два, числа, являющиеся степенями двойки, а также соседние с ними значения — от самых маленьких до громадных, вроде 216, что соответствует естественной границе во многих машинах — 16-битовому слову. Степени двойки привлекают внимание из-за того, что один из способов ускорить работу memset — устанавливать одновременно несколько байтов; это может быть выполнено с помощью специальных инструкций или посредством установки сразу не байта, а слова. Также надо проверять начальные значения массивов при различных выравниваниях — на случай, если ошибка возникает из-за стартового адреса или длины. Мы поместим используемый массив внутрь большего массива, создав тем самым некую буферную зону, или запасной отступ с каждой стороны — для того, чтобы можно было не особо ограничивать себя в выборе выравнивания.

Кроме перечисленного нам надо проверить еще множество значений для с — включая ноль, 0x7 F (самое большое значение для числа со знаком при 8-битовых байтах), 0x80 и OxFF (проверяя на потенциальные ошибки, связанные со знаковыми и беззнаковыми символами) и значения, превышающие один байт (чтобы удостовериться, что используется только один байт). Нам надо также записать в память некий шаблон, отличающийся от любого из этих значений, — с тем чтобы иметь возможность проверить, не производила ли memset запись вне границ предназначенной области.

Мы можем использовать нашу простую реализацию как стандарт для сравнения в тесте, который размещает в памяти два массива, а затем сравнивает поведение разных реализаций при разных значениях n, с и отступа внутри массива:

big = максимальная левая граница + maximum n

+ максимальная правая граница

s0 = malloc(big)

s1 = malloc(big)

для каждого значения параметров n, c и отступа offset:

установить s0 и s1 в шаблонное значение

выполнить медленный memset(s0 + offset, c, n)

выполнить быстрый memset(s1 + offset, c, n)

проверить возвращаемые значения

сравнить содержимое s0 и s1 побайтово

Ошибка, вынуждающая memset писать вне границ своего массива, скорее всего, проявится в байтах рядом с началом и концом массива, так что, оставив буферную зону, проще увидеть поврежденные байты. Уменьшается и вероятность того, что будут перезаписаны какие-то части программы в памяти. Для проверки записи вне границ мы должны проверить все значения s0 и s1, а не только те n байтов, которые должны были быть записаны.

Таким образом, наш набор тестов должен включить в себя проверку всех комбинаций значений:

offset =10, 11, ..., 20

с = 0, 1, 0x7F, 0x80, 0xFF, 0x11223344

n = 0, 1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17,

31, 32, 33.....65535, 65536, 65537

Для n должны быть подставлены, по крайней мере, значения 2i- 1, 2i и 2i + 1 для всех i от 0 до 16.

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

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

С тестированием memset связана одна история, которая может послу­жить вам хорошим уроком. Однажды мы дали копию тестов для memset одному программисту, разрабатывавшему операционную систему и биб­лиотеки для нового процессора. Через несколько месяцев мы (авторы тестов) начали работать с этой новой машиной. В какой-то момент боль­шое приложение не прошло своего набора тестов. Мы стали искать при­чины и после кропотливого труда докопались до истоков — проблема состояла в трудноуловимой неточности, связанной со знаковым расши­рением" в реализации memset на ассемблере. По непонятным причинам создатель библиотеки изменил тесты для memset, исключив из них про­верку значений, больших 0x7F. Естественно, ошибка была найдена при запуске изначальной версии теста сразу после того, как подозрение пало на memset.

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

Как в любом тестовом методе, тестовой оснастке для проверки ре­зультатов операций нужно знать правильные ответы. Важнейшим явля­ется способ, использованный нами для тестирования memset, — создание простейшей версии тестируемой функции и сравнение ее результатов с результатами основных тестов. Это можно осуществлять в несколько этапов, как будет показано в следующем примере.

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

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

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

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

Библиотека изменялась и переписывалась под разные платформы много лет, и тестирующая версия не раз оказывалась незаменимым инструментом для поиска ошибок.

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

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

Создайте, основываясь на описанных нами приемах, тестовую оснаст­ку для memset.

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

Создайте тесты для остальных функций семейства mem. .. .

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

Определите режим тестирования для числовых методов типа sqrt, sin и им подобных из библиотеки math. h. Какие вводимые значения имеют смысл? Какие независимые проверки могут быть осуществлены?

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

Определите механизмы для тестирования функций С семейства str. . . (например, strcmp). Некоторые из этих функций, особенно те, что служат для разбиения на лексемы — типа strtok или strcspn, значительно сложнее, чем функции семейства mem. .., и, следовательно, для их про­верки потребуются более изощренные тесты.

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