int* q = new int[b]; // выделяем еще немного памяти
delete[] p; // освобождаем память,
// на которую ссылается указатель p
}
Для того чтобы протестировать функцию
do_resources1
, мы должны проверить, правильно ли распределены ресурсы, т.е. освобожден ли выделенный ресурс или передан другой функции.
Перечислим очевидные недостатки.
• Файл
s
не закрыт.
• Память, выделенная для указателя
p
, не освобождается, если
b<=0
или если второй оператор new генерирует исключение.
• Память, выделенная
для указателя
q
, не освобождается, если
0<b
.
Кроме того, мы всегда должны рассматривать возможность того, что попытка открыть файл закончится неудачей. Для того чтобы получить этот неутешительный результат, мы намеренно использовали устаревший стиль программирования (функция
fopen
— это стандартный способ открытия файла в языке C). Мы могли бы упростить работу тестировщиков, если бы просто написали следующий код:
void do_resources2(int a, int b, const char* s) // менее плохой код
if (b<=0) throw Bad_arg; // может генерировать исключение
vector<int> v2(b); // создаем другой вектор (выделяем память)
}
Теперь каждый ресурс принадлежит объекту и освобождается его деструктором. Иногда, чтобы выработать идеи для тестирования, полезно попытаться сделать функцию более простой и ясной. Общую стратегию решения задач управления ресурсами обеспечивает метод RAII (Resource Acquisition Is Initialization — получение ресурса есть инициализация), описанный в разделе 19.5.2.
Отметим, что управление ресурсами не сводится к простой проверке, освобожден ли каждый выделенный фрагмент памяти. Иногда мы получаем ресурсы извне (например, как аргумент), а иногда сами передаем его какой-нибудь функции (как возвращаемое значение). В этих ситуациях довольно трудно понять, правильно ли распределятся ресурсы. Рассмотрим пример.
FILE* do_resources3(int a, int* p, const char* s) // плохая функция
// неправильная передача ресурса
{
FILE* f = fopen(s,"r");
delete p;
delete var;
var = new int[27];
return f;
}
Правильно ли, что функция
do_resources3
передает (предположительно) открытый файл обратно как возвращаемое значение? Правильно ли, что функция
do_resources3
освобождает память, передаваемую ей как аргумент
p
? Мы также добавили действительно коварный вариант использования глобальной переменной var (очевидно, указатель). В принципе передача ресурсов в функцию и из нее является довольно распространенной и полезной практикой, но для того чтобы понять, корректно ли выполняется эта операция, необходимо знать стратегию управления ресурсами. Кто владеет ресурсом? Кто должен его удалять/освобождать? Документация должна ясно и четко отвечать на эти вопросы. (Помечтайте.) В любом случае передача ресурсов изобилует возможностями для ошибок и представляет сложность для тестирования.
Обратите внимание на то, что мы (преднамеренно) усложнили пример управления ресурсами, использовав глобальную переменную.
Если в программе перемешано несколько источников ошибок, ситуация может резко ухудшиться. Как программисты мы стараемся избегать таких ситуаций. Как тестировщики — стремимся найти их.
26.3.3.3. Циклы
Мы уже рассматривали циклы, когда обсуждали функцию
binary_search
.
Большинство ошибок возникает в конце циклов.
• Правильно ли проинициализированы переменные в начале цикла?
• Правильно ли заканчивается цикл (часто на последнем элементе)?
Приведем пример, который содержит ошибку.
int do_loop(const vector<int>& v) // плохая функция
// неправильный цикл
{
int i;
int sum;
while(i<=vec.size) sum+=v[i];
return sum;
}
Здесь содержатся три очевидные ошибки. (Какие именно?) Кроме того, хороший тестировщик немедленно выявит возможности для переполнения при добавлении чисел к переменной
sum
.
Многие циклы связаны с данными и могут вызвать переполнение при вводе больших чисел.
Широко известная и особенно опасная ошибка, связанная с циклами и заключающаяся в переполнении буфера, относится к категории ошибок, которые можно перехватить, систематически задавая два ключевых вопроса о циклах.
char buf[MAX]; // буфер фиксированного объема
char* read_line // опасная функция
{
int i = 0;
char ch;
while(cin.get(ch) && ch!='\n') buf[i++] = ch;
buf[i+1] = 0;
return buf;
}
Разумеется, вы не написали бы ничего подобного! (А почему нет? Что плохого в функции
read_line
?) Однако эта ошибка, к сожалению, является довольно распространенной и имеет разные варианты.
// опасный фрагмент
gets(buf); // считываем строку в переменную buf
scanf("%s",buf); // считываем строку в переменную buf
Поищите описание функций
gets
и
scanf
в своей документации и избегайте их как чумы. Под словом “опасная” мы понимаем, что переполнение буфера является инструментом для взлома компьютеров. В настоящее время реализации выдают предупреждение об опасности использования функции
gets
и ее аналогов.
26.3.3.4. Ветвление
Очевидно, что, делая выбор, мы можем принять неправильное решение. Из-за этого инструкции
if
и
switch
являются одними из основных целей для тестировщиков. Существуют две проблемы, которые необходимо исследовать.
• Все ли возможные варианты предусмотрены?
• Правильные ли действия связаны с правильными вариантами выбора?