И все же этот тест не настолько систематический, насколько нам бы хотелось. Как-никак, мы просто выискали несколько последовательностей. Однако мы следовали некоторым правилам, которые часто полезны при работе с множествами значений; перечислим их.
• Пустое множество.
• Небольшие множества.
• Большие множества.
• Множества с экстремальным распределением.
• Множества, в конце которых происходит нечто интересное.
• Множества с дубликатами.
• Множества с четным и нечетным количеством элементов.
• Множества, сгенерированные с помощью случайных чисел.
Мы используем случайные последовательности просто для того, чтобы увидеть, повезет ли нам найти неожиданную ошибку. Этот подход носит слишком “лобовой” характер, но с точки зрения времени он очень экономный.
Почему мы рассматриваем четное и нечетное количество элементов? Дело в том, что многие алгоритмы разделяют входные последовательности на части, например на две половины, а программист может учесть только нечетное или только четное количество элементов. В принципе, если последовательность разделяется на части, то точка, в которой это происходит, становится концом подпоследовательности, а, как известно, многие ошибки возникают в конце последовательностей.
В целом мы ищем следующие условия.
• Экстремальные ситуации (большие или маленькие последовательности, странные распределения входных данных и т.п.).
• Граничные условия (все, что происходит в окрестности границы).
Реальный смысл этих понятий зависит от конкретной тестируемой программы.
Существуют две категории тестов: тесты, которые должны пройти успешно (например, поиск значения, которое есть в последовательности), и тесты, которые должны завершиться неудачей (например, поиск значения в пустой последовательности). Создадим для каждой из приведенных выше последовательностей несколько успешных и неудачных тестов. Начнем с простейшего и наиболее очевидного теста, а затем станем его постепенно уточнять, пока не дойдем до уровня, приемлемого для функции binary_search
.
int a[] = { 1,2,3,5,8,13,21 };
if (binary_search(a,a+sizeof(a)/sizeof(*a),1) == false) cout << " отказ";
if (binary_search(a,a+sizeof(a)/sizeof(*a),5) == false) cout << " отказ";
if (binary_search(a,a+sizeof(a)/sizeof(*a),8) == false) cout << " отказ";
if (binary_search(a,a+sizeof(a)/sizeof(*a),21) == false) cout << " отказ";
if (binary_search(a,a+sizeof(a)/sizeof(*a),–7) == true) cout << " отказ";
if (binary_search(a,a+sizeof(a)/sizeof(*a),4) == true) cout << " отказ";
if (binary_search(a,a+sizeof(a)/sizeof(*a),22) == true) cout << " отказ";
Это скучно и утомительно, но это всего лишь начало. На самом деле многие простые тесты — это не более чем длинные списки похожих вызовов. Положительной стороной этого наивного подхода является его чрезвычайная простота. Даже новичок в команде тестировщиков может добавить в этот набор свой вклад. Однако обычно мы поступаем лучше. Например, если в каком-то месте приведенного выше кода произойдет сбой, мы не сможем понять, где именно. Это просто невозможно определить. Поэтому фрагмент нужно переписать.
int a[] = { 1,2,3,5,8,13,21 };
if (binary_search(a,a+sizeof(a)/sizeof(*a),1) == false) cout << "1 отказ";
if (binary_search(a,a+sizeof(a)/sizeof(*a),5) == false) cout << "2 отказ";
if (binary_search(a,a+sizeof(a)/sizeof(*a),8) == false) cout << "3 отказ";
if (binary_search(a,a+sizeof(a)/sizeof(*a),21) == false) cout << "4 отказ";
if (binary_search(a,a+sizeof(a)/sizeof(*a),–7) == true) cout << "5 отказ";
if (binary_search(a,a+sizeof(a)/sizeof(*a),4) == true) cout << "6 отказ";
if (binary_search(a,a+sizeof(a)/sizeof(*a),22) == true) cout << "7 отказ";
Если вы представите себе десятки тестов, то почувствуете огромную разницу. При тестировании реальных систем мы часто должны проверить многие тысячи тестов, поэтому знать, какой из них закончился неудачей, очень важно.