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
Комментариев нет:
Отправить комментарий