Если вы представите себе десятки тестов, то почувствуете огромную разницу. При тестировании реальных систем мы часто должны проверить многие тысячи тестов, поэтому знать, какой из них закончился неудачей, очень важно.
Прежде чем идти дальше, отметим еще один пример (полуформальный) методики тестирования: мы тестировали правильные значения, иногда выбирая их из конца последовательности, а иногда из середины. Для данной последовательности мы можем перебрать все ее значения, но на практике сделать это нереально. Для тестов, ориентированных на провал, выбираем одно значение в каждом из концов последовательности и одно в середине. И снова следует отметить, что этот подход не является систематическим, хотя он демонстрирует широко распространенный образец, которому можно следовать при работе с последовательностями или диапазонами значений.
Какими недостатками обладают указанные тесты?
• Один и тот же код приходится писать несколько раз.
• Тесты пронумерованы вручную.
• Вывод минимальный (мало информативный).
Поразмыслив, мы решили записать тесты в файл. Каждый тест должен иметь идентифицирующую метку, искомое значение, последовательность и ожидаемый результат. Например:
{ 27 7 { 1 2 3 5 8 13 21} 0 }
Это тест под номером
27
. Он ищет число
7
в последовательности
{ 1,2,3,5,8,13,21 }
, ожидая, что результатом является
0
(т.е.
false
). Почему мы записали этот тест в файл, а не в текст программы? В данном случае мы вполне могли написать этот тест прямо в исходном коде, но большое количество данных в тексте программы может ее запутать. Кроме того, тесты часто генерируются другими программами. Как правило, тесты, сгенерированные программами, записываются в файлы. Кроме того, теперь мы можем написать тестовую программу, которую можно запускать с разными тестовыми файлами.
struct Test {
string label;
int val;
vector<int> seq;
bool res;
};
istream& operator>>(istream& is, Test& t); // используется описанный
// формат
int test_all(istream& is)
{
int error_count = 0;
Test t;
while (is>>t) {
bool r = binary_search( t.seq.begin(), t.seq.end(), t.val);
if (r !=t.res) {
cout << "отказ: тест " << t.label
<< "binary_search: "
<< t.seq.size() << "элементов, val==" << t.val
<< " –> " << t.res << '\n';
++error_count;
}
}
return error_count;
}
int main()
{
int errors = test_all(ifstream ("my_test.txt");
cout << "Количество ошибок: " << errors << "\n";
}
Вот как выглядят некоторые тестовые данные.
{ 1.1 1 { 1 2 3 5 8 13 21 } 1 }
{ 1.2 5 { 1 2 3 5 8 13 21 } 1 }
{ 1.3 8 { 1 2 3 5 8 13 21 } 1 }
{ 1.4 21 { 1 2 3 5 8 13 21 } 1 }
{ 1.5 –7 { 1 2 3 5 8 13 21 } 0 }
{ 1.6 4 { 1 2 3 5 8 13 21 } 0 }
{ 1.7 22 { 1 2 3 5 8 13 21 } 0 }
{ 2 1 { } 0 }
{ 3.1 1 { 1 } 1 }
{ 3.2 0 { 1 } 0 }
{ 3.3 2 { 1 } 0 }
Здесь видно, почему мы использовали строковую метку, а не число: это позволяет более гибко нумеровать тесты с помощью десятичной точки, обозначающей разные тесты для одной и той же последовательности. Более сложный формат тестов позволяет исключить необходимость повторения одной и той же тестовой последовательности в файле данных.
26.3.2.3. Случайные последовательности
Выбирая значения для тестирования, мы пытаемся перехитрить специалистов, создавших реализацию функции (причем ими часто являемся мы сами), и использовать значения, которые могут выявить слабые места, скрывающие ошибки (например, сложные последовательности условий, концы последовательностей, циклы и т.п.). Однако то же самое мы делаем, когда пишем и отлаживаем свой код. Итак, проектируя тест, мы можем повторить логическую ошибку, сделанную при создании программы, и полностью пропустить проблему. Это одна из причин, по которым желательно, чтобы тесты проектировал не автор программы, а кто-то другой.
Существует один прием, который иногда помогает решить эту проблему: просто сгенерировать много случайных значений. Например, ниже приведена функция, которая записывает описание теста в поток
cout
с помощью функции
randint()
из раздела 24.7 и заголовочного файла
std_lib.facilities.h
.
void make_test(const string& lab,int n,int base,int spread)
// записывает описание теста с меткой lab в поток cout
// генерирует последовательность из n элементов, начиная
// с позиции base
// среднее расстояние между элементами равномерно распределено
// на отрезке [0, spread]
{
cout << "{ " << lab << " " << n << " { ";