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.
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.
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:
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:
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.
All we do next for our “pseudo-random” test is we override the procedures of getting the parameters (GetFirstParam, GetSecondParam) and register in DUnit:
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:
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:
The result:
The source code of SameValue :
We change the comparison for our Random test inherited from TPlusTest:
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:
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
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.
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













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