Тестване и дебъгване

** Самостоятелно тестване на функции. 
* Данните, с които ще се тества функцията, се получават по 3 начина: 
-- от входния поток (от клавиатура или от текстов файл с пренасочване на входния поток); 
-- като стойности, получени от цикъл; 
-- случайни числа. 
* Примери за тестване на функцията squareroot за намиране на квадратен корен по метода на Херон.
Първи пример - данните идват от входния поток:

// sqrtest1.cpp 
#include
<iostream>
#include <cmath>
using namespace std;

/**
   Tests whether two floating-point numbers are 
   approximately equal.
   @param x a floating-point number
   @param y another floating-point number
   @return true if x and y are approximately equal
*/
bool approx_equal(double x, double y)
{  const double EPSILON = 1E-14;
   if (x == 0) return fabs(y) <= EPSILON;
   if (y == 0) return fabs(x) <= EPSILON;
   return fabs(x - y) / max(fabs(x), fabs(y)) <= EPSILON;
}

/* Function to be tested */
/**
   Computes the square root using Heron's formula
   @param a an integer >= 0
   @return the square root of a
*/
double squareroot(double a)
{  if (a == 0) return 0;

   double xnew = a;
   double xold;
   do
   {  xold = xnew;
      xnew = (xold + a / xold) / 2;
   }
   while (!approx_equal(xnew, xold));
   return xnew;
}

/* Test harness */
int main()
{  double x;
   while (cin >> x)
   {  double y = squareroot(x);
      cout << "squareroot of " << x << " = " << y << "\n";
   }
   return 0;
}
25 
squareroot of 25 = 5 
3 
squareroot of 3 = 1.73205 
q

Втори пример - входните стойности на функцията се генерират от цикъл.

// sqrtest2.cpp 
/* Test harness */
int main()
{  double x;
   for (x = 0; x <= 10; x = x + 0.5)
   {  double y = squareroot(x);
      cout << "squareroot of " << x << " = " << y << "\n";
   }
   return 0;
}
squareroot of 0 = 0 
squareroot of 0.5 = 0.707107 
squareroot of 1 = 1 
squareroot of 1.5 = 1.22474 
squareroot of 2 = 1.41421

Трети пример - входните стойности се получават от генератор за случайни числа. 

// sqrtest3.cpp 
#include
<iostream>
#include <cstdlib>
#include <cmath>
#include <ctime>
using namespace std;

/**
   Sets the seed of the random number generator.
*/
void rand_seed()
{  int seed = static_cast<int>(time(0));
   srand(seed);
}

/** 
   Compute a random floating point number in a range
   @param a the bottom of the range
   @param b the top of the range
   @return a random floating point number x, 
   a <= x and x <= b
*/
double rand_double(double a, double b)
{  return a + (b - a) * rand() * (1.0 / RAND_MAX);
}

/**
   Tests whether two floating-point numbers are 
   approximately equal.
   @param x a floating-point number
   @param y another floating-point number
   @return true if x and y are approximately equal
*/
bool approx_equal(double x, double y)
{  const double EPSILON = 1E-14;
   if (x == 0) return fabs(y) <= EPSILON;
   if (y == 0) return fabs(x) <= EPSILON;
   return fabs(x - y) / max(fabs(x), fabs(y)) <= EPSILON;
}

/* Function to be tested */
/**
   Computes the square root using Heron's formula
   @param a an integer >= 0
   @return the square root of a
*/
double squareroot(double a)
{  if (a == 0) return 0;

   double xnew = a;
   double xold;
   do
   {  xold = xnew;
      xnew = (xold + a / xold) / 2;
   }
   while (!approx_equal(xnew, xold));
   return xnew;
}

/* Test harness */
int main()
{  rand_seed();
   int i;
   for (i = 1; i <= 100; i++)
   {  double x = rand_double(0, 1E6);
      double y = squareroot(x);
      cout << "squareroot of " << x << " = " << y << "\n";
   }
   return 0;
}
squareroot of 185949 = 431.218 
squareroot of 680715 = 825.055 
squareroot of 17883.8 = 133.73 
squareroot of 238868 = 488.742

** Подбор на тестови примери. 
1. Докато се пише програмата, трябва да имаме прост тестов пример, на който знаем решението. 
2. Програмата се проверява с други тестови примери, също с известни решения - позитивни тестове. 
3. Включват се и граничните случаи.
- За функцията squareroot това са 0, големи числа (напр. 1Е20) и числа, близки до 0 (напр. 1е-20).
Целта е да се определят границите на параметрите, за които функцията работи вярно. 
4. Функцията се проверява с негативни тестови примери - некоректни входни данни.
- Такива за squareroot са отрицателни стойности на параметъра.
Използване на файл за запазване на тестовия вход и изпълнение с пренасочване на входния и изходния потоци.

>sqrtest1 < test.in > test.out



** Оценка на резултатите от тестването. 
* Как можем да оценим дали върнатата стойност от функцията е вярна?
- Като предварително знаем решението; 
- С програма за проверка на върнатите стойности; 
- С "оракул" - друга функция, която решава същата задача (може по-бавно и неефективно!). 
* В нашия случай можем да сравним квадрата на върнатото число от функцията с входната стойност.
Ето тестващото гнездо: 

// sqrtest4.cpp
/* Test harness */
int main()
{  int i;
   for (i = 1; i <= 100; i++)
   {  double x = rand_double(0, 1E6);
      double y = squareroot(x);
      if (
!approx_equal(y * y, x)) 
         cout << "Test failed. ";
      else 
         cout << "Test passed. ";
      cout << "squareroot of " << x << " = " << y << "\n";
   }
   return 0;
}
*  За оракул можем да използваме стандартната аритметична функция pow(x,0.5)или функцията sqrt(x). 
// sqrtest5.cpp
/* Тестващо гнездо */
 
int main() 
{ 
   rand_seed(); 
   int i; 
   for (i = 1; i <= 100; i++) 
   {  double x = rand_double(0, 1E6); 
      double y = squareroot(x); 
      if (!approx_equal(y, pow(x, 0.5))) cout << "Test failed. "; 
      else                              cout << "Test passed. "; 
      cout << "squareroot of " << x << " = " << y << "\n"; 
   } 
   return 0; 
}

** Макрос assert
Функциите често съдържат неявни предположения - напр. знаменатели трябва да са различни от нула, заплатите не трябва да бъдат отрицателни и т.н.
Такива "незаконни" стойности могат "да се вмъкнат" в програмата от входа или в резултат на предишна грешка.

Какво трябва да се направи, когато дадена функция е извикана с неподходяща стойност (напр sqrt(-1))?
Най-бруталният начин е отпечатване на съобщение и прекратяване на цялата програма.
Използване на изключения - C++ поддържа сложен механизъм, наречен обработка на изключения.
Когато е възможно, е желателно да се избегне прекъсване на програмата (въпреки, че е трудно).

Макрос assert осигурява ценна проверка на "здрав разум" в етапа на тестване на програмата.

Пример:
void raise_salary(Employee& e, double by)
{
assert(e.get_salary() >= 0 );
assert(by >= -100);

double new_salary = e.get_salary() * (1 + by / 100);
e.set_salary(new_salary);
}
Ако условието не е изпълнено, програмата завършва с полезно съобщение за грешка и показва номера на реда в текста на програмата.
assertion failed in file finclac.cpp line 61: by >= -100
Това е сигнал, че нещо се е объркало другаде и че програмата се нуждае от по-нататъшно тестване.
За да се използва макроса, трябва да се добави #include<cassert>.

** Трасиране на програмата
За да се получи разпечатка за работата на програмата, се вмъкват съобщения (контролни печати) в началото и в края на всяка функция.
Полезно е отпечатване на входните параметри, когато се влиза във функцията и отпечатване на стойността за връщане, когато се излиза от функцията.
string int_name(int n)
{ cout << "Entering digit_name. n = " << n << "\n";
...
cout << "Exiting digit name. Return value = "
<< s << "\n";
return s;
}
[trace.cpp]
Трудности при трасиране:
-- За вмъкване на подходящи контролни печати се иска доста врене.
-- Ако са добавени твърде много съобщения, ще се получи лавина от печат, който трудно се анализира.
-- Ако са добавени твърде малко съобщения, може да не се получи достатъчно информация къде е грешката.
-- Когато се завърши програмата, всички контролни печати трябва да бъдат изтрити.
-- Ако отново се появи грешка, контролните печати трябва да се пишат отново.

 Професионалните програмисти използват дебъгер за откриване на грешки в текста на програмата.

** Дебъгер
Кратко практическо ръководство за работа с GDB

CITB102