четверг, 27 ноября 2008 г.

Тестирование "белого ящика"

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

Пусть есть функция, которая получает длину сторон треугольника и возвращает его тип: равносторонний, не равносторонний или равнобедренный. В остальных случаях возвращается ошибка. Попробуем ее протестировать.

// типы треугольников
enum type_t {t_scalene=1, t_isosceles=2,
t_equilateral=3, t_error=4};

type_t triangle_type(int sideA, int sideB, int sideC)
{
// проверка валидности треугольника -
// сумма длин двух любых сторон не должна превышать
// длину третьей стороны
if((sideA>0 && sideB>0 && sideC>0) && // 1
(sideA<(sideB+sideC))&&
(sideB<(sideA+sideC))&&
(sideC<(sideA+sideB)))
{
int ab = (sideA==sideB)?1:0; // 2
int ac = (sideA==sideC)?1:0; // 3
int bc = (sideB==sideC)?1:0; // 4

int num = ab+ac+bc;

// if num==0 - scalene
// if num==1 - isosceles
// if num==3 - equilateral
type_t types [] = {t_scalene, t_isosceles,
t_error, t_equilateral};
return types[num]; // 5
}

return t_error;
}

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

Почему одного запуска функции и получение нужного результата недостаточно? Программы очень редко выполняются строчка за строчкой - этому препятствуют всевозможные логические операторы if, for, while, until, or, and, switch. Они разветвляют код, создавая сложные пути исполнения одной и той же программы при разных входящих данных. И по всем этим путям нужно пройти, один запуск с одними параметрами - это проверка одного пути из многих. Значит тестов нужно не меньше, чем перечисленных выше ключевых слов в коде. (В рассматриваемом случае под тестами я понимаю вызов функции с различными наборами параметров.)

Первый if, в приведенной выше функции, проверяет четыре условия - длины сторон треугольника должны быть неотрицательными и длина каждой из сторон должна быть меньше суммы длин двух других. Все условия объединены операторами &&, поэтому достаточно невыполнения одного из них, что бы не выполнился код, следующий за if. Но неплохо бы знать, все ли условия вычисляются правильно. Поэтому нужно передать функции значения, которые будут true и значения, которые будут false для каждого из условий. Проверить это можно по возвращаемому значению t_error, в случае если любое из условий не выполняется. Хотя первое из условий является само по себе составным, но все проверки в нем довольно простые и объеденены оператором &&, поэтому допустим, что достаточно будет проверки, когда одна длина отрицательная и когда все длины положительные.

В случае успешного прохождения if, выполняются проверки, помеченные как 2, 3, 4. Они не создают больших ответвлений, но вычисляют значения, которые влияют на выбор нужного элемента массива types в строке 5. Количество выполненных условий записывается в переменную num, которая может принимать только три значения 0, 1, 3 (2 не может быть, так как при выполнении двух автоматически выполняется третье). Значит для тестирования этой части кода достаточно 3-х наборов входящих параметров - все стороны неравны (num==0), все стороны равны (num==3), равны только 2 стороны (num==1).

Больше ответвлений в программе не наблюдается, поэтому можно подводить итог. Для тестирования функции triangle_type, нужно передать ей 14 наборов входящих параметров (8 для условия 1, 6 для условий 2, 3, 4) и проверить возвращаемый результат. После этого можно быть уверенным, что в большинстве случаев поведение функции будет предсказуемым. Хотя нет... это только часть возможных проверок. Еще можно проверять передаваемые параметры, например, как будет вести себя функция при максимально и минимально допустимых значениях, но это уже отдельная тема. Я хотел рассказать только о тестировании, базирующемся на изучении кода. Это как раз то, что теоретически отличает тестирование программистами от тестирования тестировщиками - программисты тестируют не "черный ящик".

PS: Не смотря, на то что получилось много текста, на практике это занимает не так уж много времени :)

4 комментария:

Lotrex комментирует...

Прочитал и захотелось сказать... :)
На мой взгляд, при тестировании нужно все-таки относится к функции как к "черному" ящику. То есть мы знаем, что он должен делать (что должно быть на входе и что должно быть на выходе), но не знаем, как он это делает. Тогда проверок будет гораздо больше, чем 14. Полагаю, их должно быть примерно 2^N, где N-число независимых условий. Таким образом, число тестов должно быть как минимум, 2^6 = 64. Все таки 64 набора параметров - многовато, не так ли? И думается, это от того, что функция сложновата. Возможно, будет нужно меньше проверок, чтобы протестировать 4 более простых функции:
1) возвращает true, если треугольник равнобедренный:
bool isIsosceles(int a, int b, int c);
2) возвращает true, если треугольник равносторонний:
bool isEquilateral(int a, int b, int c);
3) возвращает true, если треугольник "правильный":
bool isValid(int a, int b, int c)
4) возвращает true, если все стороны - положительные:
bool isPositive(int a, int b, int c)
Я думаю, суммарное число проверок для 4-х функций должно быть меньше.

sash_ko комментирует...

И спорить не буду и соглашаться :)
Тестирование "белого ящика" задействует в тестах весь код. Тестирование "черного ящика" делает это только теоретически.

Функция может и сложновата, но использование четырех фукнций вместо одной тоже не самый лучший вариант. Особенно отделение проверки валидности треугольника - вместо одной функции каждый раз нужно будет использовать как минимум две. А что будет если забыть isValid? Тогда надо будет протестировать каждую из 3-х оставшихся функций на невалидные данные. В этом случае суммарное число проверок врядли станет меньше

Lotrex комментирует...

Мне кажется, что проверок должно быть меньше, пока ничего доказывать не стану, потому что получится слишком длинно :) Постараюсь вскоре написать отдельный пост на эту тему, поскольку придаю тестированию большое значение.

sash_ko комментирует...

будет интересно почитать :)