Test Driven Development in Delphi: The Basics

I intend to write a Test Driven Development (TDD) series, targeted for Delphi developers. I will use DUnit, the unit testing framework for Delphi.

Note folks that the purpose of this is NOT to discuss the Pros and Cons of TDD, Unit Testing or whatsoever. The purpose is just to give a few examples. I would love if you help me when the complexity starts climbing.

If needed, for a quick understanding of what TDD or Unit Testing is, refer to the links above, or check out the book at the end of the article.

In TDD you don’t write the application code first, instead you write the test cases first. The TDD cycle is as follows:
  1. At the beginning you just write one test, and later on, more tests can be added.
  2. Make sure the initial test fails; this will validate the test harness.
  3. Write some code to pass the test. Important: don’t over-code. Just add the code needed to pass the test and period. The code does not have to be elegant at this point.
  4. Run the test: if it fails, then you have to go back to step 3 and fix your code in order to pass the test. When you succeed, then move on to step 5.
  5. Improve and optimize your code: make it elegant, more efficient, avoid duplications, etc, etc. This is called code refactoring.
  6. When refactoring your code, maybe, by accident, you break the functionality. How can you be sure that everything is working as it should? Just re-run your test and it will tell you if the previous refactoring introduced a failure or not.
  7. Go to step 1 and add a new test if needed.
The example: let’s consider the chess game. The goal will be to implement the code to verify whether a piece is placed in a valid position within the board. We are only going to implement one test: SetPositionTest.

I will number the columns (X coordinate) from 1 to 8 starting at the bottom left-hand corner. In the same way, I will number the rows (Y coordinate) from 1 to 8 starting at the bottom left-hand corner.

8
7
6
5
4
3
2
1 2 3 4 5 6 7 8

For a step by step tutorial of how to use, configure and setup DUnit you can read the English or Chinese versions of the tutorial.

Initially, the testing code should look like this:

unit ChessPiecesTests;

interface

uses
   TestFrameWork;

type
  TPieceTest = class(TTestCase)
  published
    procedure SetPositionTest;
  end;

implementation

uses
  ChessPieces;

{ TPieceTest }

procedure TPieceTest.SetPositionTest;
begin
end;

initialization
  TestFramework.RegisterTest(TPieceTest.Suite);

end.

If you run that test, it will succeed since no checks are being performed within the SetPositionTest procedure.

Each test is composed by one or more checks. I suggest adding the checks little by little. Every time you add a check, you should add business code to pass the corresponding test.

Now, let’s make the test fail on purpose. For that, let’s add one check to the SetPositionTest procedure.

procedure TPieceTest.SetPositionTest;
begin
  Check(True = False, '');
end;

True is never False. So, this test will fail. If it doesn’t fail, then something is wrong with you test harness. Fix it. You can remove this initial check once you run the test and it fails.

Now, let’s add a real check to our test. Something like this:

procedure TPieceTest.SetPositionTest;
var
  Piece: TPiece;
begin
  Piece:= TPiece.Create;
  try
    //Test trivial (normal) workflow
    Check(Piece.SetPosition(4, 4) = True, '');
  finally
    Piece.Free;
  end;
end;

If you run this test, you will get a compilation error! Yes, that’s right. You don’t have business code yet. You just have the test. This is what TDD is all about: test first, business code later. Get the point?

To avoid the compilation error, we will code a separate unit (ChessPieces) and we will add it to the uses clause of our ChessPiecesTests unit.

unit ChessPieces;

interface

type
  TPiece = class
  private
  public
    function SetPosition(aX, aY: Integer): Boolean;
  end;

implementation

{ TPiece }

function TPiece.SetPosition(aX, aY: Integer): Boolean;
begin
end;

end.

Run the test again and now the compilation error is gone. Nonetheless, the test fails, because the Piece.SetPosition(4, 4) evaluates to False.

Let’s add the minimum business code possible to pass this test:

function TPiece.SetPosition(aX, aY: Integer): Boolean;
begin
  Result:= True;
end;

This passes the test. What? Yes, this passes the test, right?

OK, what now? Well, we keep adding new checks to the test and every time this happens, we need to add new business code in order to pass it. It is very important to add checks to test the boundaries of whatever we are trying to code. I think you are getting the point, so I will just add a bunch of checks at once:

procedure TPieceTest.SetPositionTest;
var
  Piece: TPiece;
begin
  Piece:= TPiece.Create;
  try
    //Test trivial (normal) workflow
    Check(Piece.SetPosition(4, 4) = True, '');

    //Tests boundaries
    Check(Piece.SetPosition(1, 1) = True, '');
    Check(Piece.SetPosition(1, 8) = True, '');
    Check(Piece.SetPosition(8, 1) = True, '');
    Check(Piece.SetPosition(8, 8) = True, '');

    //Test beyond the boundaries
    Check(Piece.SetPosition(3, 15) = False, '');
    Check(Piece.SetPosition(3, -15) = False, '');
    Check(Piece.SetPosition(15, 3) = False, '');
    Check(Piece.SetPosition(15, 15) = False, '');
    Check(Piece.SetPosition(15, -15) = False, '');
    Check(Piece.SetPosition(-15, 3) = False, '');
    Check(Piece.SetPosition(-15, 15) = False, '');
    Check(Piece.SetPosition(-15, -15) = False, '');
  finally
    Piece.Free;
end;

The test above is even checking for the attempts of positioning a piece outside the chess board.

To pass that test let’s write some business code:

function TPiece.SetPosition(aX, aY: Integer): Boolean;
begin
  Result:= True;
  if (aY < 1) or (aY > 8) then Result:= False
  else if (aX < 1) or (aX > 8) then Result:= False;
end;

Run the test and see how it passes.

The code above could be refactored or even rewritten. The tests will remain the same, allowing us to catch any bugs introduced with the code change.

For instance, we could write the procedure above as follows:

function TPiece.SetPosition(aX, aY: Integer): Boolean;
begin
  Result:= (aX > 0) and
           (aX < 9) and 

           (aY > 0) and
           (aY < 9);
end;

Run the test, and it will tell you if this refactoring (or reimplementation) works ok.

It’s important to note that a good test should cover all possible scenarios and workflows. Pay special attention to the boundaries. At this point, a good understanding of the requisites is indispensable.

Finally, more and more tests will be needed in a real world application. Each test will have its own checks. Each test will cover one piece of the functionality: this is what unit testing is intended for.

I wrote a second article about TDD, code refactoring and design patterns in Delphi (click here). I would appreciate any comments you could provide about it.

For further reading I recommend you Test Driven Development: By Example by Kent Beck. Check it out just below:

6 comments:

  1. This was a very nice starter for TDD using DUnit.

    I have written a few thousand tests, some in TDD, but due to the system I work on, mostly tests for existing code. It is definitely the way to go and makes you think about how well you are writing your code.

    I look forward to more atricles on taking TDD and unit testing further as there are not many good articles for Delphi in this area.

    Some further points from my experience and the preferences I have when writing tests:

    The way I code would be to only put 1 check in each method. If one check breaks in your test, then you may not know if other checks further on work on not. This leads to a lot more test methods, but I think they are each simple to read if you name them correctly. Also, that way, they are really quick to write, some only 1 line.
    For example, I name all my tests
    procedure METHODNAME_TESTDESCRIPTION;

    so your tests could be:
    procedure SetPosition_NormalWorkFlow;
    procedure SetPosition_BoundaryCheck;
    procedure SetPosition_BeyondBoundaries;

    Also, when using the Check Assertions -
    instead of:
    //Test trivial (normal) workflow
    Check(Piece.SetPosition(4, 4) = True, '');

    I would write
    Check( Piece.SetPosition( 4, 4 ) );

    And for False:

    CheckFalse( Piece.SetPosition( 15, 4 ) );

    This is probably a personal preference, but I find that writing = True / False is not as easy to read as Check ( or even CheckTrue ) and CheckFalse. I suppose it depends on what you are used to.

    Plus, the message is an optional parameter, so unless you specify a message, then you dont need it, which is less code and more readable.


    I think another great point about your article is that you mention testing boundaries and BEYOND the boundaries. Too many times I read unit testing articles or guides which fail to mention this. You must know what happens when your code is passed something which will break it. What happens if your method takes a class or interface as a parameter and you pass in a NIL value? Does it handle it gracefully, raise an exception or throw an access violation? If you ask for an item in a list using an integer what happens if you pass in a negative value or a value greater than the count of items?

    ReplyDelete
    Replies
    1. After trying different things, I adopted a naming convention very similar to yours for unit tests, but even longer:

      Method_ConditionBeingTested_ExpectedResult

      Of course, I can't take credit for it. After a bunch of discussion with other developers, it's just the one that made the most sense to me. I notice that Roy Osherove uses almost the same convention, except he's clearly given it more thought than I have.

      http://osherove.com/blog/2005/4/3/naming-standards-for-unit-tests.html

      Delete
  2. Very useful topic Yanniel.

    Thanks for posting.

    Rob

    ReplyDelete
  3. You touched on what I consider to be the biggest benefit of TDD when you wrote "don’t over-code".

    It took me some time to get my head around the idea of writing tests before actually writing code. It seemed counter intuitive. After I tried it a few times for myself and saw how it changed the way I wrote code, I had a better perspective.

    TDD isn't just about testing, even though the tests are a really nice byproduct. You don't write a single line of code unless it's intended to resolve a test, and that test doesn't exist unless there is specific requirement. Unit testing isn't an arbitrary fly-by-the-seat-of-your-pants exercise invented by some ivory tower intellectual (I actually had that conversation with someone once). It's the end result of thinking through what your class or method is supposed to do and what exactly constitutes success or failure. TDD is about design, and I write better and more reliable code when I use it.

    I still see a lot of resistance to the idea of automated tests in general, let alone TDD. I'm glad to see more people talking about it.

    Thanks for the post.

    ReplyDelete
  4. On a related note, I heard about something called a "code kata", inspired by the practice forms in martial arts. It's a tool sharpening exercise where you solve a simple, but non-trivial programming problem in an effort to become intimately familiar with the mechanics of writing unit tests and code in your environment.

    http://codekata.pragprog.com/

    Presumably, you would solve the same problem multiple times to get familiar with the process, learn ways to do it faster (fewer keystrokes, less mouse use, etc) or better ways to create and refactor your code and tests. It could seem like pretty dry stuff, but if you're big on continual learning and improvement, it's not a bad way to hone your craft.

    Some people have even recorded themselves working through a kata, put it to music and posted it online.

    https://www.youtube.com/watch?v=VLEgp1189dk

    The coolest thing I learned about by watching one of these is the existence of AutoTest, which sounds like a cool thing that someone could absolutely do with Delphi.

    ReplyDelete
  5. Hey folks, I don't want to bore you ;-) I just want to thank you for your comments. They actually complemented this post and I really appreciate that. It's nice to share ideas with guys who have experience in TDD and especially with DUnit. I will write more about this topic: your comments and feedback are not only welcome, but very useful.

    ReplyDelete