Тестовые оснастки
Тестирование, о котором мы вели разговор до этого момента, относилось в основном к одной обособленной программе в завершенном виде. Это, однако, не единственный вид автоматизации тестов, так же как и не единственный способ тестирования частей большой программы в период ее написания, особенно при работе в команде. Это также и не самый эффективный способ тестирования отдельных компонентов, которые со временем должны быть объединены во что-то глобальное.
Для тестирования отдельного компонента большой программы, как правило, необходимо создать некие строительные леса (scaffold – подмости), или оснастку, которая предоставит в ваше распоряжение достаточную поддержку и достаточное взаимодействие с остальной частью системы. Мы уже приводили маленький пример подобного рода – для тестирования двоичного поиска.
Нетрудно разработать оснастку для тестирования математических, строковых функций, алгоритмов поиска и тому подобных вещей, поскольку ее создание в этих случаях сводится к установлению параметров ввода, вызову тестируемых функций и проверке результатов. Куда сложнее создать оснастку для тестирования незавершенной программы.
Чтобы проиллюстрировать повествования, мы создадим тест для memset, одной из функций семейства mem… стандартной библиотеки C/C++. Эти функции часто пишутся на языке ассемблера для,конкретных машин, поскольку их быстродействие очень важно. Однако чем более тонко они настраиваются на конкретные условия, тем больше вероятность возникновения в них ошибок и тем более тщательно должны они тестироваться.
Первый шаг – создать наиболее простые версии на С, заведомо работоспособные; они будут эталоном для сравнения быстродействия и, что более важно, проверки правильности. При переходе в новую среду разработки наши простейшие версии останутся эталоном до тех пор, пока не заработает специально созданная "родная" версия.
Функция memset (s, с, n) записывает байт с в п байтов памяти, начиная с адреса s, и возвращает s. Если нет ограничений на скорость работы, написать такую функцию,– не проблема:
Но как только главным параметром становится быстродействие, приходится прибегать к различным трюкам, вроде записи слов в 32 или 64 бита за раз. Подобные изыски могут вызвать появление ошибок, поэтому глобальное тестирование становится строго обязательным.
Тестирование базируется на комбинации всесторонних проверок (в частности, естественно, проверок граничных условий в потенциально опасных точках). Для memset граничными, очевидно, являются такие значения n, как ноль, один и два, числа, являющиеся степенями двойки, а также соседние с ними значения – от самых маленьких до громадных, вроде 216, что соответствует естественной границе во многих машинах – 16-битовому слову.
Степени двойки привлекают внимание из-за того, что один из способов ускорить работу memset – устанавливать одновременно несколько байтов; это может быть выполнено с помощью специальных инструкций или посредством установки сразу не байта, а слова. Также надо проверять начальные значения массивов при различных выравниваниях – на случай, если ошибка возникает из-за стартового адреса или длины. Мы поместим используемый массив внутрь большего массива, создав тем самым некую буферную зону, или запасной отступ с каждой стороны – для того, чтобы можно было не особо ограничивать себя в выборе выравнивания.
Кроме перечисленного нам надо проверить еще множество значений для с – включая ноль, Ox7F (самое большое значение для числа со знаком при 8-битовых байтах), 0x80 и OxFF (проверяя на потенциальные ошибки, связанные со знаковыми и беззнаковыми символами) и значения, превышающие один байт (чтобы удостовериться, что используется только один байт). Нам надо также записать в память некий шаблон, отличающийся от любого из этих значений, – с тем чтобы иметь возможность проверить, не производила ли memset запись вне границ предназначенной области.