понедельник, 23 февраля 2015 г.

What else I want to tell about TDD (not finished)

Original in Russian: http://programmingmindstream.blogspot.ru/2013/11/tdd_28.html
What else I want to tell about TDD

The previous series was here - http://programmingmindstream.blogspot.ru/2013/11/tdd.html

Many people I’ve communicated with about tests in general and about TDD in particular “crashed” on the following difficulty:

TestFirst. TestDriven.

I don’t want to “argue”, much less to disprove or criticize any of respected "theorists of TDD".nbsp;

I just want to describe my interpretation. Or – how I have understood it.

TDD says just about the following:

1. Launch ALL tests, make sure they has been done.
2. Add new test for new functionality. Make sure it has not been done.
3. Add functionality. Make sure the test has been done.

What question arises immediately?

This one: “How can I write a test for something that DOES NOT EXIST?”?

That is a FAIR question.

When I looked at TDD for the first time, I thought: - "what a bullshit" (that is an “epithet”, not an “foul term”);

How can I write test for something that DOES NOT EXIST? Let it be even NOT appropriate.

“A lady gave me a gift she had not,
And I received her gift I took not...”

I have been considering the matter for a long time and that is what I’ve decided.

There is NO TestFirst.

There is NO TestFirst.

In the FIRST place – there is a “draft” of the ARCHITECTURE. Neither code, nor tests, nor anything else, but – the “draft” of the ARCHITECTURE.

And TestFirst is made not for something “abstract”, that we’ll have “sometime later”, but for a VERY SPECIAL purpose.

 Described in the architecture and having legs growing from requirements specification (RS).

The simplest example:

 Let’s say that you need to create a “button” that brings new functionality.

Then, test can be as follows:

 AssureThatButtonExists('SomeButton');

And, OF COURSE, it won’t be done- until we add a button and will be done – WHEN we finally ADD it.

But! The test "KNOWS" that the button exists. At least, it knows its name. It means it doesn’t test “whatever”, but something certain. A part of architecture concepts. A BUTTON, described in requirements specification and in the architecture.

Let’s move on.

For example, we have to write TmyIntegerList class (I’ll analyze it in detail below).

What do we do?

We write:

“The design of the architecture” and a draft of the designed class.

EXACTLY these, but not the TEST.

Once again:

We write: “The design of the architecture” and a draft of the designed class.

EXACTLY these, but not the TEST.

Something like this:

type
 TmyIntegerList = class
  public
   procedure Add(anItem: Integer);
 end;//TmyIntegerList

procedure TmyIntegerList.Add(anItem: Integer);
begin
 Assert(false, 'Not implemented');
end;

Only AFTER THAT, we write TEST.

Once AGAIN – ONLY after we write TEST.

Let us say, of this kind:

procedure TmyIntegerListTest.ListAdd;
var
 l_List : TmyIntegerList;
begin
 l_List := TmyIntegerList.Create;
 try
  l_List.Add(47879);
 finally
  FreeAndNil(l_List);
 end;
end;

And this test – OF COURSE – WILL NOT BE DONE. And THEN, when we add IMPLEMENTATION of the method TmyIntegerList. Add – it will be done.

So.

There is no any TestFirst.

When speaking of "first" – the architecture and the design are PRIMARY, only after – the TESTS. And only after - the implementation.

So the chain of developing looks like this:
 RS -> Draft of architecture -> Test -> Code

Then, there are all possible options:
 RS -> Draft of architecture -> Test -> Code -> Test -> Code
 RS -> Draft of architecture -> Test -> Code -> Architecture -> Test -> Code
 RS -> Draft of architecture -> Test -> Code -> Architecture -> Test -> Code -> RS -> Architecture -> Test -> Code

and so on and so forth

First is - RS and the draft of the architecture – then begins – “iteration development”.

Especially delicious is that, as soon as we have written the test, we DON’T HAVE to think about “where can we check the functionability of our class”.

We have ALREADY configured all infrastructure to test it and to check.
EXACTLY this is what the word Driven in TDD means.

TESTS – HELP but not INTERFERE with the process of development.

The developer is “lead” by the tests.

Tests influence the code, and the code influences the tests.

In turn, these two TOGETHER influence the architecture and RS.

Most important is that if the tests “lead”, then, nevertheless, one way or another they are – written and they check the functionability of designed classes. It means there is NOT THE SLIGHTEST reason not to use them.

Even if you write something like this:

unit myListTests;

interface

uses
 TestFramework
 ;

type
 TmyIntegerListTests = class(TTestCase)
  published
   procedure ListAdd;
 end;//TmyIntegerListTests

implementation

procedure TmyIntegerListTests.ListAdd;
begin
 Assert(false, 'Not implemented');
end;

initialization
  TestFramework.RegisterTest(TmyIntegerListTests.Suite);

end.

Class TmyIntegerList is “kind of” not used, but it doesn’t mean that class TmyIntegerList doesn’t “exist in nature”. It exists, at least, “in your head”. If you’ve written something like TmyIntegerListTests.ListAdd, it MEANS, there is something having operation Add.

If you say to me “phew… the list of the integers… how banal… test for us the nuclear reactor…”

I’d answer - "yes, banal". But! “A journey of a thousand miles started with a first step”. And the “nuclear reactors” somewhere inside “consist of the lists of integers”.

If you’d like the REAL examples of the tests – I’ve got them. A WHOLE lot of them. I’m ready to show “in exchange for” your “at least one test”, so that to “SPEAK ON EQUAL TERMS” instead of bringing grist to the non-working mill.

Now, the remark about where, on my humble opinion, the notion of TestFirst is taken from and about “first you test what does not exist”.

It seems to me that the thing is everything comes from Java and JUnit, where reflection, duck-typing, injections and other “tricks” of this kind are used vastly.

There you can write “sort of” this test:

procedure TmyIntegerListTest.ListAdd;
var
 l_List : Object;
begin
 l_List := Framework.GetClassByName('TmyIntegerList').Create;
 try
  l_List.MethodByName.Execute('Add', [47879]);
 finally
  FreeAndNil(l_List);
 end;
end;

I am not a great expert on Java, but I hope the idea is clear.

Here, you get the “wag the dog” situation. We “kind of” know NOTHING about the tested class but, however, we can “test” it without writing it first. TestFirst. Sort of…

But let’s think – “do we really know nothing about the tested class?” OF COURSE, NO. We know its name and the fact that it has method Add.

TestFirst? Not on your life! ArchitectureFirst!!!

It does not matter that we have NOT written A SINGLE line of our class – we have ALREADY started to DESIGN its ARCHITECTURE, ONLY after – STARTED to write the test.

It does not matter that we have NOT written A SINGLE line of the code, except from the test, but! “The design of the architecture” – EXISTS when approaching with reflection or duck-typing. It EXISTS – AT LEAST “in our heads”.

But it EXISTS!

ArchitectureFirst!!!

I hope that my thought is understandable and denies all doubts about “How can I write test for something that does NOT EXIST?”

Not something that “does not exist”! But something that is being “developed and designed”. I hope it is clear.
And this does not mean that I “criticize” TDD. Quite the contrary - I am trying to do my best to ensure that as many as possible programmers are inspired with this idea.

I am trying to “deny doubts” and show that it is “not all that complicated”.

Now I will venture to give an example of “how I usually do”.

In the example I will keep to the MOST undesirable and pessimistic scenario – when either the customer or “Quality Control Group” ignore the questions of the developer.

So, the developer has either to “think up on his own”, or to write Asserts - http://18delphi.blogspot.ru/2013/04/blog-post.html

The whole code of the example is available in SVN here -  https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/TDD/Chapter0/

Let us say, we have to write the same class TmyIntegerList.  

Let’s say, its specification is as follows:

TmyIntegerList is a list of integers.

Supports operations of:

1. Inserting an item.
2. Adding an item.
3. Deleting an item.
4. Getting the number of items.
5. Getting the value of the item by its index number.

This is “sort of” - RS.

Let’s write the “first draft” of the test:

program TDD;

uses
  Vcl.Forms,
  GUITestRunner,
  myListTests in 'Tests\myListTests.pas';

{$R *.res}

begin
 Application.Initialize;
 GUITestRunner.RunRegisteredTests;
end.

unit myListTests;

interface

uses
 TestFramework
 ;

type
 TmyIntegerListTests = class(TTestCase)
  published
   procedure ListAdd;
   procedure ListInsert;
   procedure ListDelete;
   procedure ListCount;
   procedure ListItem;
 end;//TmyIntegerListTests

implementation

procedure TmyIntegerListTests.ListAdd;
begin
 Assert(false, 'Not implemented');
end;

procedure TmyIntegerListTests.ListInsert;
begin
 Assert(false, 'Not implemented');
end;

procedure TmyIntegerListTests.ListDelete;
begin
 Assert(false, 'Not implemented');
end;

procedure TmyIntegerListTests.ListCount;
begin

 Assert(false, 'Not implemented');
end;


procedure TmyIntegerListTests.ListItem;
begin
 Assert(false, 'Not implemented');
end;

initialization
  TestFramework.RegisterTest(TmyIntegerListTests.Suite);
end.

-- it this draft we “kind of do not know” WHAT we test, but, in fact, the “draft of the architecture” is ALREADY sitting “in our head”. We know about the methods Add, Insert, Delete, Count and Item.

And, of course, the test is not done.

Let us state the “draft of the architecture” in the designed class.

Exactly – let us “put on paper” what “was in our brain” when we’ve been writing the first test.

Let’s describe the “prototype” of our class:

unit myIntegerList;

interface

type
 TmyIntegerList = class
  public
   type
    IndexType = Integer;
    ItemType = Integer;
  protected
   function pm_GetCount: IndexType;
   function pm_GetItem(anIndex: IndexType): ItemType;
  public
   procedure Add(anItem: ItemType);
   procedure Insert(anIndex: IndexType; anItem: ItemType);
   procedure Delete(anIndex: IndexType);
   property Count: IndexType
    read pm_GetCount;
   property Items[anIndex: IndexType]: ItemType
    read pm_GetItem;
 end;//TmyIntegerList

implementation

// TmyIntegerList

function TmyIntegerList.pm_GetCount: IndexType;
begin
 Result := -1;
 Assert(false, 'Not implemented');
end;

function TmyIntegerList.pm_GetItem(anIndex: IndexType): ItemType;
begin
 Result := -1;
 Assert(false, 'Not implemented');
end;

procedure TmyIntegerList.Add(anItem: ItemType);
begin
 Assert(false, 'Not implemented');
end;

procedure TmyIntegerList.Insert(anIndex: IndexType; anItem: ItemType);
begin
 Assert(false, 'Not implemented');
end;

procedure TmyIntegerList.Delete(anIndex: IndexType);
begin
 Assert(false, 'Not implemented');
end;

end.

And let’s modify the tests:

unit myIntegerListTests;

interface

uses
 TestFramework
 ;

type
 TmyIntegerListTests = class(TTestCase)
  published
   procedure ListAdd;
   procedure ListInsert;
   procedure ListDelete;
   procedure ListCount;
   procedure ListItem;
 end;//TmyIntegerListTests

implementation

uses
 System.SysUtils,
 myIntegerList
 ;

procedure TmyIntegerListTests.ListAdd;
var
 l_List : TmyIntegerList;
begin
 l_List := TmyIntegerList.Create;
 try
  l_List.Add(Random(1000));
 finally
  FreeAndNil(l_List);
 end;//try..finally
end;

procedure TmyIntegerListTests.ListInsert;
var
 l_List : TmyIntegerList;
begin
 l_List := TmyIntegerList.Create;
 try
  l_List.Insert(0, Random(1000));
 finally
  FreeAndNil(l_List);
 end;//try..finally
end;

procedure TmyIntegerListTests.ListDelete;
var
 l_List : TmyIntegerList;
begin
 l_List := TmyIntegerList.Create;
 try
  l_List.Delete(0);
 finally
  FreeAndNil(l_List);
 end;//try..finally
end;

procedure TmyIntegerListTests.ListCount;
var
 l_List : TmyIntegerList;
begin
 l_List := TmyIntegerList.Create;
 try
  l_List.Count;
 finally
  FreeAndNil(l_List);
 end;//try..finally
end;

procedure TmyIntegerListTests.ListItem;
var
 l_List : TmyIntegerList;
begin
 l_List := TmyIntegerList.Create;
 try
  l_List.Item[0];
 finally
  FreeAndNil(l_List);
 end;//try..finally
end;

initialization
  TestFramework.RegisterTest(TmyIntegerListTests.Suite);

end.

-- at this point, we already KNOW about the REAL designed class and use it.

Tests in this state SURELY will not be done.

Let’s move on.

Let’s implement at least one of the methods of our designed class.

Or rather – TWO of them, the simplest ones - Add and Count.

That’s how it looks like:

unit myIntegerList;
 
interface
 
type
 TmyIntegerList = class
  public
   type
    IndexType = Integer;
    ItemType = Integer;
  private
   type
    ItemsArray = array of ItemType;
  private
   f_Items : ItemsArray;
  protected
   function pm_GetCount: IndexType;
   function pm_GetItem(anIndex: IndexType): ItemType;
  public
   procedure Add(anItem: ItemType);
   procedure Insert(anIndex: IndexType; anItem: ItemType);
   procedure Delete(anIndex: IndexType);
   property Count: IndexType
    read pm_GetCount;
   property Item[anIndex: IndexType]: ItemType
    read pm_GetItem;
 end;//TmyIntegerList
 
implementation
 
// TmyIntegerList
 
function TmyIntegerList.pm_GetCount: IndexType;
begin
 Result := Length(f_Items);
end;
 
function TmyIntegerList.pm_GetItem(anIndex: IndexType): ItemType;
begin
 Result := -1;
 Assert(false, 'Not implemented');
end;
 
procedure TmyIntegerList.Add(anItem: ItemType);
begin
 SetLength(f_Items, Length(f_Items) + 1);
 f_Items[High(f_Items)] := anItem;
end;
 
procedure TmyIntegerList.Insert(anIndex: IndexType; anItem: ItemType);
begin
 Assert(false, 'Not implemented');
end;
 
procedure TmyIntegerList.Delete(anIndex: IndexType);
begin
 Assert(false, 'Not implemented');
end;
 
end.

-- we’ll see that two tests – have been DONE. Not necessarily correctly done, but they has been DONE.

Here we happen to deal with what is called Driven.

We’ve filled the “skeleton” of the prototype with the “flesh” and got a response from tests at once.

Let’s move on.

Let’s implement the other methods of our designed class:

unit myIntegerList;

interface

type
 TmyIntegerList = class
  public
   type
    IndexType = Integer;
    ItemType = Integer;
  private
   type
    ItemsArray = array of ItemType;
  private
   f_Items : ItemsArray;
  protected
   function pm_GetCount: IndexType;
   function pm_GetItem(anIndex: IndexType): ItemType;
  public
   procedure Add(anItem: ItemType);
   procedure Insert(anIndex: IndexType; anItem: ItemType);
   procedure Delete(anIndex: IndexType);
   property Count: IndexType
    read pm_GetCount;
   property Item[anIndex: IndexType]: ItemType
    read pm_GetItem;
 end;//TmyIntegerList

implementation

// TmyIntegerList

function TmyIntegerList.pm_GetCount: IndexType;
begin
 Result := Length(f_Items);
end;

function TmyIntegerList.pm_GetItem(anIndex: IndexType): ItemType;
begin
 Result := f_Items[anIndex];
end;

procedure TmyIntegerList.Add(anItem: ItemType);
begin
 SetLength(f_Items, Length(f_Items) + 1);
 f_Items[High(f_Items)] := anItem;
end;

procedure TmyIntegerList.Insert(anIndex: IndexType; anItem: ItemType);
begin
 if (anIndex = Self.Count) then
  Add(anItem)
 else
  Assert(false, 'Not implemented');
 // - I really don’t know what I am to do here since RS is kind of INCOMPLETE
end;

procedure TmyIntegerList.Delete(anIndex: IndexType);
begin
 if (anIndex < 0) OR (anIndex >= Self.Count) then
 // - there is really nothing to delete
  Exit
 else
  Assert(false, 'Not implemented');
end;

end.

We launch the tests, and what do we see?

ONE test – has NOT been done - TmyIntegerListTests.ListItem - AV has occurred there.

The reason is in TmyIntegerList.pm_GetItem method.

What are we to do?

Let’s rewrite method TmyIntegerList.pm_GetItem in this way:

function TmyIntegerList.pm_GetItem(anIndex: IndexType): ItemType;
begin
 if (Self.Count = 0) then
  // - I really don’t know what to do since RS does not specify it
  Result := Random(5676)
 else
  Result := f_Items[anIndex];
end;

We launch the tests and see – they have been DONE!

Have we done our work? "Probably". But not a fact.

Suppose, we have sent our application to “Quality Control Group” and it found the following:

var
 l_List : TmyIntegerList;
begin
 l_List := TmyIntegerList.Create;
 try
  l_List.Add(Random(54365));
  l_List.Delete(0);
 finally
  FreeAndNil(l_List);
 end;//try..finally
end;

And “opened the ticket in QC with number 1”.

After having “got the details” and the negotiations with QCG – we make sure that, however, there is an error.

We write the NEW TEST:

procedure TmyIntegerListTests.QCTicket1;
var
 l_List : TmyIntegerList;
begin
 l_List := TmyIntegerList.Create;
 try
  l_List.Add(Random(54365));
  l_List.Delete(0);
 finally
  FreeAndNil(l_List);
 end;//try..finally
end;

And we make sure that, however , it is NOT done!

What are we to do?

We edit the code of the designed class:

procedure TmyIntegerList.Delete(anIndex: IndexType);
begin
 if (anIndex < 0) OR (anIndex >= Self.Count) then
 // - there’s really nothing to delete
  Exit
 else
 if (anIndex = Self.Count - 1) then
  SetLength(f_Items, Self.Count - 1)
 else
  Assert(false, 'I really don’t know what am I to do here');
end;

We launch the tests - and they are done. Have we done our work? I DON’T KNOW. Probably…

Now let’s suppose, our QCG has found one more error:

... to be continued ...

Should I continue? Or the idea is clear?

----

Discussion - https://plus.google.com/u/0/113567376800896602748/posts/8pG2gJNkG7F

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

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