Проверка входных данных подпрограмм и тестирование

Материал из Вики ИТ мехмата ЮФУ
Перейти к: навигация, поиск


Введение

Рассмотрим задачу:
Описать процедуру Mean(X, Y, AMean, GMean), вычисляющую среднее арифметическое AMean = (X+Y)/2 и среднее геометрическое GMean = (X·Y)1/2 двух положительных чисел X и Y (X и Y — входные, AMean и GMean — выходные параметры вещественного типа).

Проверка входных данных: Assert

Обратите внимание, что положительность параметров X и Y нужна для вычисления среднего геометрического, которое происходит внутри процедуры Mean. Никто не гарантирует, что на вход процедуре будут передаваться только корректные входные данные, но правильная работа самой процедуры — забота разработчика этой процедуры. Он должен «обезопасить» себя от неверных входных данных, поэтому их проверка должна находиться именно внутри процедуры Mean.

Мы знаем, как проверять входные данные: можно использовать оператор Assert. Рекомендуется использовать оператор Assert для каждого параметра отдельно. То есть в данном примере должно быть два оператора:

Assert(x > 0);
Assert(y > 0);

а не

Assert((x > 0) and (y > 0));

Тестирование подпрограммы с помощью Assert

Прелесть алгоритма, заключенного в подпрограмме, состоит в том, что его легко вызывать многократно с разными входными значениями. Это позволяет нам перейти к более надежному способу тестирования. Теперь мы можем «зашить» в основную программу вызовы подпрограммы с необходимыми тестовыми значениями.

Пример

Итак, пусть процедура Mean написана. У неё два входных и два выходных параметра. Значит каждый тестовый пример должен проверять, что при заданных значениях X и Y оба выходных параметра имеют ожидаемые значения. То есть:

  1. нужно задать значения входных параметров (X, Y);
  2. вызвать процедуру Mean, передав ей эти входные значения;
  3. проверить, что значения выходных параметров (AMean, GMean) совпадают с ожидаемыми.

Попробуем это запрограммировать. Начнём с простейшего примера: для X, Y | X = Y должны быть получены значения AMean = X, GMean = X.

  var x, y: real;
  var am, gm: real;
  
  // (1) задаём значения входных параметров
  x := 2;
  y := x;
  // (2) вызываем процедуру
  Mean(x, y, am, gm);
  // (3) проверяем корректность выходных значений

Вопрос: как будем выполнять проверку (шаг 3)? Вспомним, что при решении задач с рядами мы уже выполняли проверку правильности найденного значения с помощью оператора Assert. В качестве параметра нужно передать логическое выражение, значение которого истинно, если решение верно. Нам нужно проверить, что AMean = X, GMean = X. Может быть так?

  // (3) проверяем корректность выходных значений
  Assert((am = x) and (gm = x));

Математически это верно, но вещественные числа так не сравнивают на равенство. А у нас как раз вещественные числа. Исправим. Введем некоторую константу Eps (равную, например, значению 0.00000001), тогда проверка будет выглядеть так:

  // (3) проверяем корректность выходных значений
  Assert((Abs(am - x) < Eps) and (Abs(gm - x) < Eps));

Сравнивать вещественные значения нам понадобится не один раз (по крайней мере нужно написать ещё несколько тестов). Это отличный претендент на подпрограмму.
Для двух данных вещественных чисел нам нужно знать, равны они или нет. Значит подпрограмма должны иметь по крайней мере два входных вещественных параметра и один выходной — логического типа (так как нам нужен только ответ да/нет). Выходной параметр один, поэтому удобно использовать функцию. Кроме того, удобно передавать в качестве входного параметра и точность сравнения, ведь в разных задачах может требоваться разная точность.

/// Возвращает истину, если вещественные числа x, y равны с точностью eps
function AreEquals(x, y: real; eps: real): boolean;
begin
  Result := Abs(x - y) < eps;
end;

Теперь используем эту функцию для проверки работы процедуры Mean:

  // (3) проверяем корректность выходных значений
  Assert(AreEquals(am, x, Eps) and AreEquals(gm, x, Eps));

Таким образом полный код для одного тестового примера выглядит так:

  var x, y: real;
  var am, gm: real;
  
  // (1) задаём значения входных параметров
  x := 2;
  y := x;
  // (2) вызываем процедуру
  Mean(x, y, am, gm);
  // (3) проверяем корректность выходных значений
  Assert(AreEquals(am, x, Eps) and AreEquals(gm, x, Eps));

Добавим еще парочку тестовых примеров:

  // test 2: different values
  x := 3;
  y := 27;
  Mean(x, y, am, gm);
  Assert(AreEquals(am, 15, Eps) and AreEquals(gm, 9, Eps));
  // test 3: different values
  x := 6.05;
  y := 5;
  Mean(x, y, am, gm);
  Assert(AreEquals(am, 5.525, Eps) and AreEquals(gm, 5.5, Eps));

Замечание. Обратите внимание, что в двух последних примерах значения переменных x и y не используются. Поскольку это входные параметры, можем переписать код следующим образом:

  // test 2: different values
  Mean(3, 27, am, gm);
  Assert(AreEquals(am, 15, Eps) and AreEquals(gm, 9, Eps));
  // test 3: different values
  Mean(6.05, 5, am, gm);
  Assert(AreEquals(am, 5.525, Eps) and AreEquals(gm, 5.5, Eps));

Зачем нужно такое тестирование?

Важными преимуществами таких «вшитых» тестов перед ручным тестированием (которое мы делали раньше) является то, что эти тесты:
(a) выполняются автоматически и
(b) выполняются всегда.

Вы написали тесты один раз и больше не заботитесь об этом. Если по каким-то причинам вы изменили подпрограмму, запускать её вручную на всех наборах тестовых данных не нужно, это будет выполнено автоматически. И если после изменения вдруг что-то «сломалось», то при запуске программы это будет сразу же обнаружено.

Резюме

Проверка правильности работы подпрограммы — отдельная задача, задача тестирования. Поэтому тестирование также можно выделить в специальную подпрограмму, чтобы не «замусоривать» тестами основную программу. А в основную программу остаётся только включить вызов тестирующей подпрограммы. Таким образом полный код может выглядеть так:

/// Вычисляет среднее арифметическое и геометрическое чисел x, y
procedure Mean(...);
begin
  Assert(x > 0);
  Assert(y > 0);
  // вычисление AMean, GMean
  // ...
end;

/// Возвращает истину, если вещественные числа x, y равны с точностью eps
function AreEquals(x, y: real; eps: real): boolean;
begin
  Result := Abs(x - y) < eps;
end;

/// Тестирует работу процедуры Mean
procedure TestMean(eps: real);
begin
  var x, y: real;
  var am, gm: real;
  // test 1: equal values
  x := 2;
  y := x;
  Mean(x, y, am, gm);
  Assert(AreEquals(am, x, eps) 
    and AreEquals(gm, x, eps));
  // test 2: different values
  Mean(3, 27, am, gm);
  Assert(AreEquals(am, 15, Eps) 
    and AreEquals(gm, 9, Eps));
  // test 3: different values
  Mean(6.05, 5, am, gm);
  Assert(AreEquals(am, 5.525, Eps) 
    and AreEquals(gm, 5.5, Eps));
end;

const Eps = 0.00000001;

begin
  // вызов тестирующей процедуры
  TestMean(Eps);
  
  // Основная программа
  // ...
end.

Тестирование функций

Проверка корректности чистых функций (у которых все параметры являются входными и нет побочных эффектов вроде печати на консоль) происходит несколько проще, так как вызов функции сам является выражением, значение которого нужно проверить. Поэтому тестирование функции intMin (минимум двух целых чисел) может быть таким:

Assert(intMin(3, 8) = 3);
Assert(intMin(-20, 0) = -20);
Assert(intMin(-20, -100) = -100);
Assert(intMin(0, 0) = 0);
Assert(intMin(5, 5) = 5);

Тестирование предикатов

Предикат это частный случай функции, а именно, функция, возвращающая значение типа boolean. Для их тестирования не требуется даже выполнять сравнение. Приведём пример для предиката проверки целого числа на то, что оно является двухзначным. Обратите внимание, что вторым аргументом Assert удобно передавать сообщение с текстом проверяемого условия, чтобы при возникновении ошибок сразу становилось понятно, с каким значением не справилась проверяемая функция. Это замечание относится к любым Assert.

Assert(HasTwoDigits(-99),  'HasTwoDigits: -99 >>> true');
Assert(HasTwoDigits(-60),  'HasTwoDigits: -60 >>> true');
Assert(HasTwoDigits(-10),  'HasTwoDigits: -10 >>> true');
Assert(HasTwoDigits(10),   'HasTwoDigits:  10 >>> true');
Assert(HasTwoDigits(11),   'HasTwoDigits:  11 >>> true');
Assert(HasTwoDigits(87),   'HasTwoDigits:  87 >>> true');
Assert(HasTwoDigits(99),   'HasTwoDigits:  99 >>> true';
Assert(not HasTwoDigits(-35672),   'HasTwoDigits: -35672 >>> false');
Assert(not HasTwoDigits(-100),     'HasTwoDigits: -100 >>> false'); 
Assert(not HasTwoDigits(-9),       'HasTwoDigits: -9 >>> false');  
Assert(not HasTwoDigits(-1),       'HasTwoDigits: -1 >>> false');    
Assert(not HasTwoDigits(0),        'HasTwoDigits: 0 >>> false'); 
Assert(not HasTwoDigits(6),        'HasTwoDigits: 6 >>> false'); 
Assert(not HasTwoDigits(100),      'HasTwoDigits: 100 >>> false'); 
Assert(not HasTwoDigits(8000),     'HasTwoDigits: 8000 >>> false');

Ссылки