среда, 18 марта 2015 г.

Testing of calculator №7. Comparing of floating-point numbers. Details about tests architecture

Original in Russian: http://programmingmindstream.blogspot.com/2014/06/7.html

Table of contents

Having drawn the diagram of classes to the previous chapter I’ve noticed I also have TRandomPlusTest class which I haven’t seen in GUI of DUnit.



unit RandomPlusTest;
 
interface
 
uses
  PlusTest
  ;
 
type
  TRandomPlusTest = class(TPlusTest)
   protected
    function  GetFirstParam: Single; override;
    function  GetSecondParam: Single; override;
  end;//TRandomPlusTest
 
implementation
 
uses
  TestFrameWork,
  SysUtils
  ;
 
function TRandomPlusTest.GetFirstParam: Single;
begin
 Result := 1000 * Random;
end;
 
function TRandomPlusTest.GetSecondParam: Single;
begin
 Result := 2000 * Random;
end;
 
initialization
 //TestFramework.RegisterTest(TRandomPlusTest.Suite);
 
end.

I’ve looked into the source code and has seen our class is not registered in DUnit. We remove the comment and launch the test.


As you can see, the test has not passed. Let’s look at the details. We select our test and launch it a number of times. Our “random” test does not always fail.


Let’s recollect the classes hierarchy for GUI-testing.


First, let’s look at TFirstTest. It has been directly inherited from TTestCase and it executes one method DoIt.

unit FirstTest;
 
interface
 
uses
  TestFrameWork
  ;
 
type
  TFirstTest = class(TTestCase)
   published
    procedure DoIt;
  end;//TFirstTest
 
implementation
 
procedure TFirstTest.DoIt;
begin
 Check(true);
end;
 
initialization
 TestFramework.RegisterTest(TFirstTest.Suite);
 
end.

We need the first test to “check the work of infrastructure”. In this case, after launching DoIt, we know for sure our test has been registered and it passes.

Then, a more fun architecture begins.
DUnit only launches published procedures (that is it’s character), in which the check is executed. Let’s look closer at our next (first descendant of TTestCase) class TCalculatorGUITest:

unit CalculatorGUITest;
 
interface
 
uses
  TestFrameWork,
  MainForm
  ;
 
type
  TCalculatorGUITest = class(TTestCase)
   protected
    procedure VisitForm(aForm: TfmMain); virtual; abstract;
   published
    procedure DoIt;
  end;//TCalculatorGUITest
 
implementation
 
uses
  Forms
  ;
 
procedure TCalculatorGUITest.DoIt;
var
 l_Index : Integer;
begin
 for l_Index := 0 to Screen.FormCount do
  if (Screen.Forms[l_Index] Is TfmMain) then
  begin
   VisitForm(Screen.Forms[l_Index] As TfmMain);
   break;
  end;//Screen.Forms[l_Index] Is TfmMain
end;
 
end.

As we can see, there’s only published procedure DoIt. Namely it will be executed for all descendants. It will also call the abstract procedure VisitForm, which we have to write in the descendant.
I’d like to give special attention to the fact that we do not register our class in DUnit.

The next is TOperationTest class that implements forms visiting (protected), but it is also not registered in testing framework:

unit OperationTest;
 
interface
 
uses
  CalculatorGUITest,
  MainForm
  ;
 
type
  TOperation = (opAdd, opMinus, opMul, opDiv, opDivInt);
 
  TOperationTest = class(TCalculatorGUITest)
   protected
    procedure VisitForm(aForm: TfmMain); override;
    function  GetOp: TOperation; virtual; abstract;
    function  GetFirstParam: Single; virtual;
    function  GetSecondParam: Single; virtual;
  end;//TOperationTest
 
implementation
 
uses
  TestFrameWork,
  Calculator,
  SysUtils
  ;
 
function TOperationTest.GetFirstParam: Single;
begin
 Result := 10;
end;
 
function TOperationTest.GetSecondParam: Single;
begin
 Result := 20;
end;
 
procedure TOperationTest.VisitForm(aForm: TfmMain);
var
 aA, aB : Single;
begin
 aA := GetFirstParam;
 aB := GetSecondParam;
 aForm.edtFirstArg.Text := FloatToStr(aA);
 aForm.edtSecondArg.Text := FloatToStr(aB);
 case GetOp of
  opAdd:
  begin
   aForm.btnAdd.Click;
   Check((aForm.edtResult.Text) = TCalculator.FloatToStr(aA + aB));
  end;
  opMinus:
  begin
   aForm.btnMinus.Click;
   Check((aForm.edtResult.Text) = TCalculator.FloatToStr(aA - aB));
  end;
  opMul:
  begin
   aForm.btnMul.Click;
   Check((aForm.edtResult.Text) = TCalculator.FloatToStr(aA * aB));
  end;
  opDiv:
  begin
   aForm.btnDiv.Click;
   Check((aForm.edtResult.Text) = TCalculator.FloatToStr(aA / aB));
  end;
  opDivInt:
  begin
   aForm.btnDivInt.Click;
   Check((aForm.edtResult.Text) = TCalculator.FloatToStr(Round(aA) div Round(aB)));
  end;
 end;//case GetOp
end;
 
end.

Finally, we’ve got to the tests. In tests, for example in TPlusTest, we only define the required method GetOp. BUT !!!
We register our test in DUnit.

unit PlusTest;
 
interface
 
uses
  OperationTest
  ;
 
type
  TPlusTest = class(TOperationTest)
   protected
    function  GetOp: TOperation; override;
  end;//TPlusTest
 
implementation
 
uses
  TestFrameWork,
  SysUtils
  ;
 
function TPlusTest.GetOp: TOperation;
begin
 Result := opAdd;
end;
 
initialization
 TestFramework.RegisterTest(TPlusTest.Suite);
 
end.

All we do next for our “pseudo-random” test is we override the procedures of getting the parameters (GetFirstParam, GetSecondParam) and register in DUnit:

unit RandomPlusTest;
 
interface
 
uses
  PlusTest
  ;
 
type
  TRandomPlusTest = class(TPlusTest)
   protected
    function  GetFirstParam: Single; override;
    function  GetSecondParam: Single; override;
  end;//TRandomPlusTest
 
implementation
 
uses
  TestFrameWork,
  SysUtils
  ;
 
function TRandomPlusTest.GetFirstParam: Single;
begin
 Result := 1000 * Random;
end;
 
function TRandomPlusTest.GetSecondParam: Single;
begin
 Result := 2000 * Random;
end;
 
initialization
 TestFramework.RegisterTest(TRandomPlusTest.Suite);
 
end.

Having considered the architecture, let’s get back to our “failure”. As seen from the code above, for the random test we take “any” two numbers (TOperationTest.VisitForm), execute the operation on them using ButtonClick and then compare with the result of addition converted into a string.

Sure, we will not always have the equation. Thing is, many fractional decimal numbers can not be correctly given with nulls and ones of the digital computer.

At this point, we finally get to the core of our article – comparing of floating-point numbers.

This problem has been much discussed. For the first time, I learned about it from the Code complete (12.3. Floating-Point Numbers) by Steve McConnel, although I’ve never faced it in my work. Steve’s example remains actual until now:

program DoubleEqualsExample;
{$APPTYPE CONSOLE}
{$R *.res}
uses
  System.SysUtils;
var
 nominal, sum : double;
 i: byte;
begin
 nominal := 1.0;
 sum := 0;
 for I := 1 to 10 do
  sum := sum + 0.1;
 
 if sum = nominal
  then Writeln('Equals sum=' + FloatToStr(sum) + ' nominal=' + FloatToStr(nominal))
  else Writeln('NOT Equals sum=' + FloatToStr(sum) + ' nominal=' + FloatToStr(nominal));
 
 Readln;
end.

The result of the application’s work:


If we “follow in the master’s steps”, then we’ll print the value sum at each iteration:


As we can see, despite the fact that Delphi rounds our sum to one, it prints another number at the end.
Then, it gets a bit technical. Since I was not the only person to read MacConnel and Delphi developers did read it too, the version of comparing floating-point numbers was taken into account.

Math.pas  unit has such procedures for comparison:
- SomeValue 
- CompareValue
- IsZero

All three functions are intended for comparison with a special accuracy of Epsilon set by user. We check it using our example:

...
 if SameValue(sum, nominal, 0.00000001)
  then Writeln('Equals sum=' + FloatToStr(sum) + ' nominal=' + FloatToStr(nominal))
  else Writeln('NOT Equals sum=' + FloatToStr(sum) + ' nominal=' + FloatToStr(nominal));
...

The result:


The source code of SameValue :

function SameValue(const A, B: Double; Epsilon: Double): Boolean;
begin
  if Epsilon = 0 then
    Epsilon := Max(Min(Abs(A), Abs(B)) * DoubleResolution, DoubleResolution);
  if A > B then
    Result := (A - B) <= Epsilon
  else
    Result := (B - A) <= Epsilon;
end;

We change the comparison for our Random test inherited from TPlusTest:

...
const
 c_Epsilon = 0.0001;
...
  opAdd:
  begin
   aForm.btnAdd.Click;
   Check(SameValue(StrToFloat(aForm.edtResult.Text), (aA + aB), c_Epsilon));
  end;
...

After having launched the test (a number of times), we make sure all is OK:


By analogue we add random of GUI-tests for all operations. I will not give the code because it is quite similar to TRandomTest.

We launch all tests:


All tests except integer division test failed. We correct code of VisitForm, taking the “comparison” into account:


As we can see, there's a problem left with multiplication test. If we multiply “random of a number” from our application in Windows calculator:


we see the error is 1/10.



The comparison of the operations for multiplying with required error:

unit OperationTest;
 
interface
 
uses
  CalculatorGUITest,
  MainForm
  ;
 
type
  TOperation = (opAdd, opMinus, opMul, opDiv, opDivInt);
 
  TOperationTest = class(TCalculatorGUITest)
   protected
    procedure VisitForm(aForm: TfmMain); override;
    function  GetOp: TOperation; virtual; abstract;
    function  GetFirstParam: Single; virtual;
    function  GetSecondParam: Single; virtual;
  end;//TOperationTest
 
implementation
 
uses
  TestFrameWork,
  Calculator,
  SysUtils,
  Math;
 
const
 c_Epsilon = 0.0001;
 c_MulEpsilon = 0.1;
 
function TOperationTest.GetFirstParam: Single;
begin
 Result := 10;
end;
 
function TOperationTest.GetSecondParam: Single;
begin
 Result := 20;
end;
 
procedure TOperationTest.VisitForm(aForm: TfmMain);
var
 aA, aB : Single;
begin
 aA := GetFirstParam;
 aB := GetSecondParam;
 aForm.edtFirstArg.Text := FloatToStr(aA);
 aForm.edtSecondArg.Text := FloatToStr(aB);
 case GetOp of
  opAdd:
  begin
   aForm.btnAdd.Click;
   Check(SameValue(StrToFloat(aForm.edtResult.Text), (aA + aB), c_Epsilon));
  end;
  opMinus:
  begin
   aForm.btnMinus.Click;
   Check(SameValue(StrToFloat(aForm.edtResult.Text), (aA - aB), c_Epsilon));
  end;
  opMul:
  begin
   aForm.btnMul.Click;
   Check(SameValue(StrToFloat(aForm.edtResult.Text), (aA * aB), c_MulEpsilon));
  end;
  opDiv:
  begin
   aForm.btnDiv.Click;
   Check(SameValue(StrToFloat(aForm.edtResult.Text), (aA / aB), c_Epsilon));
  end;
  opDivInt:
  begin
   aForm.btnDivInt.Click;
   Check(SameValue(StrToFloat(aForm.edtResult.Text), (Round(aA) div Round(aB)), c_Epsilon));
  end;
 end;//case GetOp
end;
 
end.

Let’s sum up.

Floating-point numbers will not always be equal, even if they look identical at sight.
Most solutions are provided by standard libraries, so hurry to reinvent the wheel. RTFM :)
In case of two double’s multiplication , find out the accuracy of calculation from the customer.

Some more about our GUI-tests architecture. The final diagram looks like this:


TCalculatorGUITest registers the procedure DoIt for all descendants in DUnit, and it starts the procedure of testing. TOperationTest is actually an abstract class, though it has the whole logic of operations check. Classes - TPlusTest, TMinusTest, ..., etc. are registered in DUnit and are final tests, through the inheritance mechanism. Despite the fact that the whole logic of “correctness check” is in descendant. All Random tests are the expanded version of simple tests, though due to overloading of operations GetFirstParam and GetSecondParam they can act in special cases. In this situation, each class implements pseudo-random input data.

The repository.
p.s.
Useful links:
Numerical methods with FORTRAN iv case studies, W. S. Dorn and D. D. McCracken, Wiley, London, 1972
http://mat.net.ua/mat/biblioteka/McKraken-Dorn-Chislennie-metodi.djvu
http://stackoverflow.com/questions/6106119/how-to-compare-double-in-delphi

Repository

Комментариев нет:

Отправить комментарий