четверг, 7 июня 2012 г.

Как писать юнит-тесты к программе на Free Pascal при помощи FPTest

В свете работы над моими старыми программами из КГУ понадобилось покрывать код юнит-тестами. Как выяснилось после гуглопоиска, для Free Pascal, которым я компилирую свою переработку, существует проект под названием FPTest, представляющий собой каркас для написания юнит-тестов на этом замечательном языке.

Документации по этому проекту довольно немного, и официальная вики, как и README, сильно Lazarus-ориентированы. Поэтому расскажу здесь, как подключить FPTest к существующему консольному проекту и собрать тесты, написанные с его помощью.

Допустим, у нас есть программа

Я разберу первое задание из курса «алгоритмизация и программирование» в КГУ за 2006 год.

Требуется написать программу, которая будет получать число от пользователя с консоли, вычислять значение достаточно сложной математической функции от неё и выдавать результат обратно на консоль. Как-то так:


program Lesson01;
uses
   SysUtils;

var
   x: real;
   answer: real;
   
begin
   writeln('Enter 0.1 =< x <= 0.6 here:');
   repeat
      readln(x)
   until
      (x>=0.1) and (x<=0.6);
   
   answer:= MyFunction(x);
   
   writeln('Answer is ',answer:12:3);
end.

Понятное дело, MyFunction где-то определена. Для того, чтобы подключить юнит-тесты, нужно осознать то, что запускать тесты будет другая программа, которую тоже нужно компилировать. То есть, каждый билд будет собирать две программы: одна собственно та, которая нужна и другая, которая запускает юнит-тесты и рапортует о результатах. Поэтому весь код, который будет покрыт юнит-тестами, выносим в отдельный .pas файл, который будет использоваться как программой с тестами, так и основной программой.

Итого, состав файлов конечного решения задачи выглядит так:

  1. Lesson01.pas — точка входа основной программы.
  2. Tests01.pas — точка входа юнит-тестов.
  3. L01Code.pas — код программы, проверяемый тестами.
  4. L01Tests.pas — код самих юнит-тестов (а вы как думали? :) ).

Я здесь не буду говорить о том, как писать сами юнит-тесты, используя FPTest. Только о том, как их подключить.

Разнесём код по отдельным файлам

Приведу полные листинги всех четырёх файлов, это важно, потому что boilerplate code, нужный для нормальной работы FPTest, не особо очевиден:

Lesson01.pas

program Lesson01;
uses
   SysUtils,
   L01Code;

var
   x: real;
   answer: real;
   
begin
   writeln('Enter 0.1 =< x <= 0.6 here:');
   repeat
      readln(x)
   until
      (x>=0.1) and (x<=0.6);
   
   answer:= MyFunction(x);
   
   writeln('Answer is ',answer:12:3);
end.
Tests01.pas

program Tests01;

uses
   TextTestRunner,
   L01Tests;

begin
   L01Tests.RegisterTests;
   RunRegisteredTests;
end.
L01Tests.pas

unit L01Tests;

interface 

uses
   TestFramework, L01Code;

type
   Lesson1Tests = class(TTestCase)
   published    
      procedure TestValidInput;
      procedure TestNegativeXShouldThrowError;
      procedure TestMinimumAllowedX;
      procedure TestMaximumAllowedX;
   end;

procedure RegisterTests;

implementation
uses
   math;

const
   precision = 0.0001;

procedure RegisterTests;
begin
   TestFramework.RegisterTest(Lesson1Tests.Suite);
end;

procedure Lesson1Tests.TestValidInput;
begin
   Check(MyFunction(0.1) - 12.4244944 < precision, 'On the left edge function should be calculable');
   Check(MyFunction(0.35) - 4.8590140 < precision, 'Function should be calculable in the middle of interval');
   Check(MyFunction(0.6)  - 4.8087379 < precision, 'On the right edge function should be calculable');
end; 

procedure Lesson1Tests.TestNegativeXShouldThrowError;
begin
   StartExpectingException(EInvalidArgument);
   MyFunction(-1);
   StopExpectingException();
end;

procedure Lesson1Tests.TestMinimumAllowedX;
begin
   StartExpectingException(EInvalidArgument);
   MyFunction(0.1 - precision);
   StopExpectingException();
end;   

procedure Lesson1Tests.TestMaximumAllowedX;
begin
   StartExpectingException(EInvalidArgument);
   MyFunction(0.6 + precision);
   StopExpectingException();
end;   

               
initialization
end.
L01Code.pas

unit L01Code;

interface

function MyFunction(x: real) : real;

implementation

uses
   Math;

function MyFunction(x: real) : real;
begin
   if (x < 0.1) then
      raise EInvalidArgument.Create('Expected x in [0.1, 0.6]');
   if (x > 0.6) then
      raise EInvalidArgument.Create('Expected x in [0.1, 0.6]');
   MyFunction := abs(sin(power(10.5*x, 1/2))) / (power(x, 2/3) - 0.143) + 2*pi*x;
end;

initialization
end.

Как обычно, тестов больше, чем кода.

Связывание вместе

Теперь, когда у нас все эти файлы счастливо лежат в одной папке, нам нужно собирать программу с тестами, указывая компилятору, где лежит папка с исходниками FPTest. Я вот не знал, что для этого нужно использовать параметр -Fu:


$ fpc -Fu"ПУТЬ ДО FPTEST" -Mobjfpc Tests01.pas

Путь можно относительный. Обязательно использовать флаг -Mobjfcp, потому что FPTest у себя внутри использует объектную модель, так что без -Mobjfpc тесты не соберутся.

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


$ fpc Lesson01.pas