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

Testing of calculator №6.2.1. Applying of “classical TDD”

Original in Russian: http://programmingmindstream.blogspot.com/2014/06/621-tdd_11.html

Table of contents

In this new chapter, we’ll try to apply “classical TDD” in developing of a new operation on the calculator.
Our customer has informed us that he’d like the calculator to divide integers. He paid an advance, stressed the point that it had to be ready by the next day and took himself wings before we could ask him a question.

Questions I have got:
- What comes up in the application?
It seems clear, we have to implement operation div.

- Does the function has boundary conditions?
Since we were not told about the previous functions too, and the customer seems to be satisfied, so we do by analogy.

- Do we have to process “large” numbers that, for example, exceed BigInt?
The answer is as to the above question.

- Does the behavior of other functions change?
Since we were not told about it, let’s suppose so far that nothing changes.

- How does the interface of the application change, how will the new operation button look like and where will it be put?
We decided to just add the button nearby and name it DivInt.

Next, let’s state our requirements specification. We’ll create a folder in Google Docs and enable the customer to access it. Since Google commits the changes in the document, we can track what changes have been made and by whom. We could also add the file to GIT.

So, let’s get directly to coding.

First, let’s refresh our memory about TDD. “Classical TDD” describes steps in this way:
1. Writing of a new test. Making sure it does not pass.
2. Writing of the code. Making sure test passes.
3. Launching of all tests. Making sure the old tests work correctly.
4. Refactoring if necessary.


Let’s get started.

Step 1. Writing a new test. We add the new test to TCalculatorOperationViaLogicTest because this class is responsible for checking of business logic.

procedure TCalculatorOperationViaLogicTest.TestDivInt;
begin
  Assert(false, 'Not Implemented')
end;

We launch our tests and see the red test.


Next, we add the new operation DivInt to our class TCalculator, transfer our Assert to business-logic class and call our new operation in the test.

...
class function TCalculator.DivInt(const A, B: string): string;
begin
  Assert(false, 'Not Implemented');
end;
...
procedure TCalculatorOperationViaLogicTest.TestDivInt;
var
  x1, x2 : string;
begin
  x1:= cA;
  x2:= cB;
  CheckTrue(2 = StrToFloat(TCalculator.DivInt(x2, x1)));
end;

What has been done at this point:
1. We have defined WHERE we write the test.
2. We have defined WHERE we’ll implement business logic for the new operation.

Step 2. We write business logic in our class TCalculator

class function TCalculator.DivInt(const A, B: string): string;
var
  x1, x2, x3  : Integer;
begin
  x1 := StrToInt(A);
  x2 := StrToInt(B);
  x3 := x1 div x2;
  Result := FloatToStr(x3);
end;

We launch the tests and see the green test:


At this point, we’ve made first but serious error that will reveal itself a bit later. Meanwhile I’d like to draw your attention to the type of the variables x1, x2, x3 : Integer;

Next, we’ve decided to add checking of operation to the rest of our tests, first of all, to TCalculatorOperationViaEtalonTest class, i.e. we state the implementation of the operation to the etalon.

procedure TCalculatorOperationViaEtalonTest.TestDivInt;
var
  x1, x2  : string;
begin
  x1:= cA;
  x2:= cB;
 
  CheckOperation(g_Logger, x1, x2, TCalculator.DivInt);
end;

After the green test has been executed, it’s the turn of tests with pseudo-random data that were discussed in the previous article. And the fun’s beginning.

Having added the new test and expecting no tricks, we see the following:

procedure TCalculatorOperationRandomSequenceTest.TestDivInt;
begin
  CheckOperationSeq(g_Logger, TCalculator.DivInt);
end;



As we can see, the test passes successfully, therefore our procedure passes Float. I have chosen Integer for TCalculator.DivInt procedure. My next unwise decision was “distorting” of our tests so that everything “coincides”. As a result, we've got the following.

We had:

procedure TCalculatorOperationRandomSequenceTest.CheckOperationSeq(
  aLogger: TLogger;
  anOperation: TCalcOperation);
var
  l_Index : Integer;
begin
  RandSeed := 40000;
  aLogger.OpenTest(Self);
  for l_Index := 0 to 10000 do
    CheckOperation(aLogger,
                   1000 * Random,
                   2000 * Random + 1, anOperation);
  CheckTrue(aLogger.CheckWithEtalon);
end;

We have:

procedure TCalculatorOperationRandomSequenceTest.CheckOperationSeq(
  aLogger: TLogger;
  anOperation: TCalcOperation);
var
  l_Index : Integer;
begin
  RandSeed := 40000;
  aLogger.OpenTest(Self);
  for l_Index := 0 to 10000 do
  begin
    if Self.GetName = 'TestDivInt' then
      CheckOperation(aLogger,
                     Int(2000 * Random),
                     Int(1000 * Random + 1), anOperation)
    else
      CheckOperation(aLogger,
                     1000 * Random,
                     2000 * Random + 1, anOperation)
  end;
  CheckTrue(aLogger.CheckWithEtalon);
end;


As we can see, the problem has been solved. But, we’ve made a fundamental mistake in implementing of business logic when we started to “distort” the testing to get “green lamps”. We should not forget to add etalons to git.

What did we actually have to do initially? We should have found out from the client what we wanted to see on introducing 2 real numbers and pressing the new button?
Since time is pressing and there’s nothing, we decide to correct the business logic by simple rounding of the numbers before executing div operation.

class function TCalculator.DivInt(const A, B: string): string;
var
  x1, x2, x3  : Integer;
begin
  x1 := round(StrToFloat(A));
  x2 := round(StrToFloat(B));
  x3 := x1 div x2;
  Result := FloatToStr(x3);
end;

Our procedure of sequence checking gains its the initial form.

procedure TCalculatorOperationRandomSequenceTest.CheckOperationSeq(
  aLogger: TLogger;
  anOperation: TCalcOperation);
var
  l_Index : Integer;
begin
  RandSeed := 40000;
  aLogger.OpenTest(Self);
  for l_Index := 0 to 10000 do
    CheckOperation(aLogger,
                   1000 * Random,
                   2000 * Random + 1, anOperation);
  CheckTrue(aLogger.CheckWithEtalon);
end;

We delete the etalon, make sure tests pass and get to work with GUI of our application. We add a button on a form and at the same time put in order the names of controls:

procedure TfmMain.btnDivIntClick(Sender: TObject);
begin
 edtResult.Text := TCalculator.DivInt(edtFirstArg.Text, edtSecondArg.Text);
end;

At last, we check the work of the form. Of course, we can just press our buttons since the business logic was already checked by 10k versions. However, to prove that we are “true programmers”, we add one more test:

...
type
  TOperation = (opAdd, opMinus, opMul, opDiv, opDivInt);
...
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;

We launch tests, check if they coincide and commit our changes to git.

The source code of our files:

unit Calculator;
 
interface
 
type
 TCalculator = class
  public
   class function Add(const A, B: string): string;
   class function Sub(const A, B: string): string;
   class function Mul(const A, B: string): string;
   class function Divide(const A, B: string): string;
   class function FloatToStr(aValue: Double): string;
   class function DivInt(const A, B: string): string;
 end;//TCalculator
 
implementation
 
uses
  SysUtils
  ;
 
class function TCalculator.FloatToStr(aValue: Double): string;
var
 l_FS : TFormatSettings;
begin
  l_FS := TFormatSettings.Create;
  l_FS.DecimalSeparator := '.';
  Result := SysUtils.FloatToStr(aValue, l_FS);
end;
 
class function TCalculator.Add(const A, B: string): string;
var
  x1, x2, x3 : single;
begin
  x1 := StrToFloat(A);
  x2 := StrToFloat(B);
  x3 := x1 + x2;
  Result := FloatToStr(x3);
end;
 
class function TCalculator.Sub(const A, B: string): string;
var
  x1, x2, x3 : single;
begin
  x1 := StrToFloat(A);
  x2 := StrToFloat(B);
  x3 := x1 - x2;
  Result := FloatToStr(x3);
end;
 
class function TCalculator.Mul(const A, B: string): string;
var
  x1, x2, x3 : single;
begin
  x1 := StrToFloat(A);
  x2 := StrToFloat(B);
  x3 := x1 * x2;
  Result := FloatToStr(x3);
end;
 
class function TCalculator.Divide(const A, B: string): string;
var
  x1, x2, x3 : single;
begin
  x1 := StrToFloat(A);
  x2 := StrToFloat(B);
  x3 := x1 / x2;
  Result := FloatToStr(x3);
end;
 
class function TCalculator.DivInt(const A, B: string): string;
var
  x1, x2, x3  : Integer;
begin
  x1 := round(StrToFloat(A));
  x2 := round(StrToFloat(B));
  x3 := x1 div x2;
  Result := FloatToStr(x3);
end;
 
end.


unit CalculatorOperationViaLogicTest;
 
interface
 
uses
  TestFrameWork,
  Calculator
  ;
 
 type
  TCalculatorOperationViaLogicTest = class(TTestCase)
   published
    procedure TestDiv;
    procedure TestMul;
    procedure TestAdd;
    procedure TestSub;
    procedure TestSubError;
    procedure TestDivInt;
  end;//TCalculatorOperationViaLogicTest
 
implementation
 
  uses
   SysUtils;
 
const
 cA = '5';
 cB = '10';
{ TCalculatorOperationViaLogicTest }
 
procedure TCalculatorOperationViaLogicTest.TestDiv;
var
  x1, x2 : string;
begin
  x1:= cA;
  x2:= cB;
  CheckTrue(2 = StrToFloat(TCalculator.Divide(x2, x1)));
end;
 
procedure TCalculatorOperationViaLogicTest.TestDivInt;
var
  x1, x2 : string;
begin
  x1:= cA;
  x2:= cB;
  CheckTrue(2 = StrToFloat(TCalculator.DivInt(x2, x1)));
end;
 
procedure TCalculatorOperationViaLogicTest.TestSub;
var
  x1, x2 : string;
begin
  x1:= cA;
  x2:= cB;
  CheckTrue(5 = StrToFloat(TCalculator.Sub(x2, x1)));
end;
 
procedure TCalculatorOperationViaLogicTest.TestSubError;
var
  x1, x2 : string;
begin
  x1:= cA;
  x2:= cB;
  CheckFalse(7 = StrToFloat(TCalculator.Sub(x2, x1)));
end;
 
procedure TCalculatorOperationViaLogicTest.TestMul;
var
  x1, x2: string;
begin
  x1:= cA;
  x2:= cB;
  CheckTrue(50 = StrToFloat(TCalculator.Mul(x2, x1)));
end;
 
procedure TCalculatorOperationViaLogicTest.TestAdd;
var
  x1, x2  : string;
begin
  x1:= cA;
  x2:= cB;
  CheckTrue(15 = StrToFloat(TCalculator.Add(x2, x1)));
end;
 
initialization
 TestFramework.RegisterTest(TCalculatorOperationViaLogicTest.Suite);
end.


unit CalculatorOperationViaEtalonTest;
 
interface
 
uses
  TestFrameWork,
  Calculator,
  Tests.Logger;
 
 type
  TCalcOperation = function (const A, B: string): string of object;
 
  TCalculatorOperationViaEtalonTest = class(TTestCase)
   private
    procedure CheckOperation(aLogger: TLogger;
                             aX1, aX2: string;
                             anOperation : TCalcOperation);
   published
    procedure TestDiv;
    procedure TestMul;
    procedure TestAdd;
    procedure TestSub;
    procedure TestDivInt;
  end;//TCalculatorOperationViaEtalonTest
 
implementation
 
  uses
    SysUtils;
 
 
const
 cA = '5';
 cB = '10';
{ TCalculatorOperationViaEtalonTest }
procedure TCalculatorOperationViaEtalonTest.CheckOperation(aLogger: TLogger;
                                                           aX1, aX2: string;
                                                           anOperation : TCalcOperation);
begin
  aLogger.OpenTest(Self);
  aLogger.ToLog(aX1);
  aLogger.ToLog(aX2);
  aLogger.ToLog(anOperation(aX1,aX2));
  CheckTrue(aLogger.CheckWithEtalon);
end;
 
procedure TCalculatorOperationViaEtalonTest.TestDiv;
var
  x1, x2 : string;
begin
  x1:= cA;
  x2:= cB;
 
  CheckOperation(g_Logger, x1, x2, TCalculator.Divide);
end;
 
procedure TCalculatorOperationViaEtalonTest.TestDivInt;
var
  x1, x2  : string;
begin
  x1:= cA;
  x2:= cB;
 
  CheckOperation(g_Logger, x1, x2, TCalculator.DivInt);
end;
 
procedure TCalculatorOperationViaEtalonTest.TestSub;
var
  x1, x2  : string;
begin
  x1:= cA;
  x2:= cB;
 
  CheckOperation(g_Logger, x1, x2, TCalculator.Sub);
end;
 
procedure TCalculatorOperationViaEtalonTest.TestMul;
var
  x1, x2  : string;
begin
  x1:= cA;
  x2:= cB;
 
  CheckOperation(g_Logger, x1, x2, TCalculator.Mul);
end;
 
procedure TCalculatorOperationViaEtalonTest.TestAdd;
var
  x1, x2  : string;
begin
  x1:= cA;
  x2:= cB;
 
  CheckOperation(g_Logger, x1, x2, TCalculator.Add);
end;
 
initialization
 TestFramework.RegisterTest(TCalculatorOperationViaEtalonTest.Suite);
end.


unit CalculatorOperationRandomSequenceTest;
 
interface
 
uses
  TestFrameWork,
  Calculator,
  Tests.Logger;
 
 type
  TCalcOperation = function (const A, B: string): string of object;
 
  TCalculatorOperationRandomSequenceTest = class(TTestCase)
   private
    procedure CheckOperation(aLogger: TLogger;
                             aX1, aX2: Double;
                             anOperation : TCalcOperation);
    procedure CheckOperationSeq(aLogger: TLogger;
                                anOperation : TCalcOperation);
   published
    procedure TestDiv;
    procedure TestMul;
    procedure TestAdd;
    procedure TestSub;
    procedure TestDivInt;
  end;//TCalculatorOperationRandomSequenceTest
 
implementation
 
  uses
    SysUtils;
 
{ TCalculatorOperationRandomSequenceTest }
procedure TCalculatorOperationRandomSequenceTest.CheckOperationSeq(
  aLogger: TLogger;
  anOperation: TCalcOperation);
var
  l_Index : Integer;
begin
  RandSeed := 40000;
  aLogger.OpenTest(Self);
  for l_Index := 0 to 10000 do
    CheckOperation(aLogger,
                   1000 * Random,
                   2000 * Random + 1, anOperation);
  CheckTrue(aLogger.CheckWithEtalon);
end;
 
 
procedure TCalculatorOperationRandomSequenceTest.CheckOperation(
  aLogger: TLogger;
  aX1, aX2: Double;
  anOperation : TCalcOperation);
begin
  aLogger.ToLog(aX1);
  aLogger.ToLog(aX2);
  aLogger.ToLog(anOperation(FloatToStr(aX1),FloatToStr(aX2)));
end;
 
procedure TCalculatorOperationRandomSequenceTest.TestDiv;
begin
  CheckOperationSeq(g_Logger, TCalculator.Divide);
end;
 
procedure TCalculatorOperationRandomSequenceTest.TestSub;
begin
  CheckOperationSeq(g_Logger, TCalculator.Sub);
end;
 
procedure TCalculatorOperationRandomSequenceTest.TestMul;
begin
  CheckOperationSeq(g_Logger, TCalculator.Mul);
end;
 
procedure TCalculatorOperationRandomSequenceTest.TestAdd;
begin
  CheckOperationSeq(g_Logger, TCalculator.Add);
end;
 
procedure TCalculatorOperationRandomSequenceTest.TestDivInt;
begin
  CheckOperationSeq(g_Logger, TCalculator.DivInt);
end;
 
initialization
 TestFramework.RegisterTest(TCalculatorOperationRandomSequenceTest.Suite);
end.


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


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.

Let's sum up.
We have considered the real-world situation and showed how developing through testing with “classical TDD” is done. We have faced a real-world problem that, by the way, can be solved not only as shown above. You will see more solutions in future for we’ll change the architecture of our tests. In general, I believe our product is ready to be accepted by customer and I can sleep in peace unbothered by night calls :).

Repository

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

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