Издательский дом ООО "Гейм Лэнд"СПЕЦВЫПУСК ЖУРНАЛА ХАКЕР #53, АПРЕЛЬ 2005 г.

Непсихологические тесты

Крис Касперски ака мыщъх

Спецвыпуск: Хакер, номер #053, стр. 053-060-2


Тест должен задействовать все ветви программы, чтобы после его выполнения не осталось ни одной незадействованной строчки кода. Отношение кода, который хотя бы раз получил выполнение, к общему коду программы называется покрытием (coverage), и для его измерения придумано множество инструментов - от профилировщиков, входящих в штатный комплект поставки компиляторов, до самостоятельных пакетов, лучшим из которых является NuMega True Coverage.

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

Всегда транслируй программу с максимальным уровнем предупреждений (для Microsoft Visual C++ это ключ /W4), обращая внимание на все сообщения компилятора. Некоторые наиболее очевидные ошибки обнаруживаются уже на этом этапе. Сторонние верификаторы кода (lint, smatch) еще мощнее и распознают ошибки, с которыми трансляторы уже не справляются.

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

Регистрация ошибок

Завалить программу - проще всего. Зафиксировать обстоятельства сбоя намного сложнее. Типичная ситуация: тестер прогоняет программу через серию тестов, непройденные тесты отправляются разработчику, чтобы тот локализовал ошибку и исправил баги. Но у разработчика эти же тесты проходят успешно! А… он уже все переделал, перекомпилировал с другими ключами и т.д. Чтобы этого не происходило, используй системы управления версиями - Microsoft Source Safe или *nix'овый CVS.

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

Самыми коварными являются "плавающие" ошибки, проявляющиеся с той или иной степенью вероятности: девятьсот прогонов программа проходит нормально, а после них неожиданно падает без всяких видимых причин. Многопоточные приложения и код, управляющий устройствами ввода/вывода, порождает особый класс невоспроизводимых ошибок, некоторые из которых могут проявляться лишь раз в несколько лет (!). Вот типичный пример:

Пример плавающей ошибки

char *s;

f1() {int x=strlen(s); s[x]='*'; s[x+1] = 0;} // поток 1

f2() {printf("%s\n",s);} // поток 2

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

Назад на стр. 053-060-1  Содержание  Вперед на стр. 053-060-3