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





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