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.
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.
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
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.
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:
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:
We have:
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.
Our procedure of sequence checking gains its the initial form.
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:
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:
We launch tests, check if they coincide and commit our changes to git.
The source code of our files:
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
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
Комментариев нет:
Отправить комментарий