Home > XP > Busting the great TDD myth

Busting the great TDD myth

November 20th, 2008

WARNING
There’s a lot going on in this article. It is intended to be humorous and slightly inflammatory. It’s also the first of a series. I intend to show you how the average Joe digs himself into a hole and then how to get out. I hope you enjoy the read.

On with the show …

You will not learn what you need to know from a TDD course/book/lecture. When you walk away you will not be able to write better software. You will not be able to test drive all your code in a fully automated test suite. Your expectations will not be met and you will quickly dump the whole concept of Test Driven Development.

It’s true.

The problem has nothing to do with TDD. The problem is that you don’t know how to write better software. TDD will not make you a coding rock star. You will not be the envy of your peers. Your face will not be printed on a t-shirt (I don’t think that’s ever happened but it would be cool).


So what’s the hype with TDD, BDD, ATDD, LMNOP, QRS, ETC? The truth is to be successful with TDD you must either 1) already be good at writing great software or 2) get good at writing great software. If you’re already good at writing great software and/or already being successful with TDD then you’re probably reading this for entertainment. As such I’ll forgo any further exposition on that point. :)

So how do you get good and writing great software? Examine the aspects of code that make software difficult or impossible to test. Software that is testable has a far greater chance at being great software than code that is not testable.

What are the aspects of code that software difficult to test? While there are many the primary culprit is Dependancies. How is the expressed as a symptom of non-testablity in code? It comes primarily from the mixing of object creation and business logic.

Let’s see an example of this:

You’re fresh out of TDD school (or whatever) and you sit back down at your desk at work to tackle your first code task. It’s a simple task - log in to the system. You think “No problem!” and create your first test case:

procedure TLoginTests.TestGoodLogin;
var
  login : TLogin;
begin
  login := TLogin.Create;
  CheckEquals(True, login.Execute('fred', 'flinstone'), 'Login should have succeeded!');
  FreeAndNil(login);
end;

Looks easy enough, right? You run the test, it fails and all seems right with the world! Now you add some code to make the test pass:

type
  TLogin = class(TObject)
  public
    function Execute(const UserName, Password : string) : Boolean;
  end;
 
implementation
 
function TLogin.Execute(const UserName, Password : string) : Boolean;
begin
  Result := True;
end;

You run the test and see it pass! You’re happy that you followed the method, did the simplest thing possible to make the test pass and now you’re ready for the next step: Refactor. You examine the code and decide you don’t really need to do any refactoring right now. Go Team!!

Now you write the next test:

procedure TLoginTests.TestBadLogin;
begin
  login := TLogin.Create;
  CheckEquals(False, login.Execute('fred', 'rubble'), 'Login should have failed!');
end;

You run the test and see it fail. Huzzah! Now it’s time to add the code to make the test work. You think for a second about simply making the function return false but you know better than that. So what is the simplest thing? You need to query the database, retrieve the record for this user and validate the password, right?

function TLogin.Execute(const UserName, Password : string) : Boolean;
var
  UserQuery : TADOQuery;
  DBUserPassword : string;
begin
  UserQuery := TADOQuery.Create(nil);
  UserQuery.ConnectionString := 'Driver={SQL Native Client};' +
                                'Server=localhost;' +
                                'Database=SecurityDatabase;' +
                                'Uid=myUsername;' +
                                'Pwd=myPassword;';
  UserQuery.SQL.Text := 'SELECT Password FROM Users WHERE UserName = ' + 
                         QuotedStr(UserName);
  UserQuery.Open;
  DBUserPassword := UserQuery.FieldByName('Password').AsString;
  UserQuery.Free;
  Result := DBUserPassword = Password;
end;

(You’re welcome to beat the SQL injection horse to death in the comments)

Whew! You run the tests and find both tests fail. Ouch. Feeling silly, you add the user “fred” into the database with a password of “flinstone” and run the tests again. Success! The tests pass. Now it’s time for refactoring.

The most obvious problem is that your production code can’t be hard coded to talk to your local database with a bad set of login credentials. Looks like the hard coded connection string had got to go:

  ...
  UserQuery.ConnectionString := GetConnectionString;
  ...
 
function TLogin.GetConnectionString : string;
var
  reg : TRegIniFile;
begin
  reg := TRegIniFile.Create('Software\MyApp');
  Result := reg.ReadString('Database', 'ConnectionString', '');
  FreeAndNil(reg);
end;

This time you were a smarty pants and added the registry entry before running the tests! You find the tests still pass. Sweet! TDD rocks!

Any more refactoring needed? Nothing stands out. Any more tests to write? We’ve a test for a good login and a bad login … that seems sufficient. Let’s check it in and get the tests added to the build system.

You arrive the next morning ready to tackle your next challenge but find an email from the build system politely informing you that you broke the build. Damn. The tests passed on my machine. Stupid build system. So what’s the problem? The test failed with an exception - cannot connect to the database. Damn, guess we needed another test after all. You know that your code reads the connection string from the registry and you know that the connection in your registry is correct. That means you need to save your good connection string, write a bad (or blank) connection string, execute the code and put the good connection string back:

procedure TLoginTests.TestDatabaseConnectionFailure;
begin
  reg := TRegIniFile.Create('Software\MyApp');
  OldConnectionString := reg.ReadString('Database', 'ConnectionString', '');
  reg.WriteString('Database', 'ConnectionString', 'NO CONNECTION FOR YOU!!');
  login := TLogin.Create;
  CheckEquals(False, login.Execute('This', 'Does Not Matter'));
  reg.WriteString('Database', 'ConnectionString', OldConnectionString);
  FreeAndNil(login);
  FreeAndNil(reg);
end;

Hmm, that was alot trouble to go to simulate no connection to the database. You run the tests and they blow up. Cool. Now you can fix the problem:

function TLogin.Execute(const UserName, Password : string) : Boolean;
var
  UserQuery : TADOQuery;
  DBUserPassword : string;
begin
  UserQuery := TADOQuery.Create(nil);
  try
    UserQuery.ConnectionString := GetConnectionString;
    UserQuery.SQL.Text := 'SELECT Password FROM Users WHERE UserName = ' + 
                         QuotedStr(UserName);
    UserQuery.Open;
    DBUserPassword := UserQuery.FieldByName('Password').AsString;
    UserQuery.Free;
    Result := DBUserPassword = Password;
  except
    Result := False;
  end;
end;

That ought to do it. Running the tests confirms it - the code now gracefully handles no connection to the database. The tests pass so let’s check it in. Hmmm, what a bunch of work that was. Seems like the test code was a little complicated.

Another build cycle completes and you get another broken build email. Yikes, that’s two! The test for the bad login passed while the test for the good login failed. You know that you’re handling the no-connection scenario so it can’t be that. The test database must be missing the user with which you’re driving your test. Inspecting the database proves your theory. You add the user “fred” with password “flinstone”. So now, provided you can connect to the database you should be golden … hmmm, connect to the database. Better check that the build machine has the connection string in the registry. You don’t want a third broken build email. Sure enough, no connection string in the registry. You quietly add it and pray that nothing else goes wrong.

You sure have to jump though a lot of hoops to make this TDD stuff work. To have gotten this all right the first time you needed to have:

  1. Checked in the code
  2. Added the user into the test database
  3. Added the connection string to the registry on the build machine

You come in the next day to another broken build email. What the ?!?!?!!! Again, the test for the bad login passed while the test for the good login failed. How can that possibly be? After some investigation you find that another test is clearing the Users table and leaves it empty before your test runs. Wow. So in your test you have to guarantee that the user you need in your test exists:

procedure TLoginTests.TestGoodLogin;
var
  login : TLogin;
  reg : TRegIniFile;
  UserQuery : TADOQuery;
begin
  UserQuery := TADOQuery.Create(nil);
  reg := TRegIniFile.Create('Software\MyApp');
  UserQuery.ConnectionString := reg.ReadString('Database', 'ConnectionString', '');
  reg.Free;
  UserQuery.SQL.Text := 'INSERT INTO Users VALUES(' +
    QuotedStr('fred') + ',' + QuotedStr('flinstone') + ')';
  UserQuery.ExecSQL;
  UserQuery.Free;
  login := TLogin.Create;
  CheckEquals(True, login.Execute('fred', 'flinstone'), 'Login should have succeeded!');
  FreeAndNil(login);
end;

You run your test and it blows up - the query is complaining that the user “fred” already exists. Grrr:

  ...
  try
    UserQuery.ExecSQL;
  except
    // shhhh.
  end;
  ...

Now your test passes. Just to be sure, you delete the user “fred” from your database and test again: success. Good grief!

You sure had to do a lot of work to make that simple set of tests passing. Let me give you a glimpse into the future of what those tests will look like:

procedure TLoginTests.TestGoodLogin;
{
var
  login : TLogin;
  reg : TRegIniFile;
  UserQuery : TADOQuery;
}
begin
  Check(True);
{
  UserQuery := TADOQuery.Create(nil);
  reg := TRegIniFile.Create('Software\MyApp');
  UserQuery.ConnectionString := reg.ReadString('Database', 'ConnectionString', '');
  reg.Free;
  UserQuery.SQL.Text := 'INSERT INTO Users VALUES(' +
    QuotedStr('fred') + ',' + QuotedStr('flinstone') + ')';
  try
    UserQuery.ExecSQL;
  except
    // shhhh.
  end;
  UserQuery.Free;
  login := TLogin.Create;
  CheckEquals(True, login.Execute('fred', 'flinstone'), 'Login should have succeeded!');
  FreeAndNil(login);
}
end;
 
procedure TLoginTests.TestBadLogin;
begin
  Check(True);
{
  login := TLogin.Create;
  CheckEquals(False, login.Execute('fred', 'rubble'), 'Login should have failed!');
}
end;

The punch-line

So how did this turn into such a nightmare? Shouldn’t this have been an easy proposition? Is TDD to blame for the trouble and the extended completion time-line?

Lets look at the problems:

The dependencies: the data in the database and the connection string in the registry. That little bit of code that read a user from the database and a connection string from the registry created a testing nightmare.

The mixing of concerns: the login code had to be concerned with business logic as well as where to find the needed pieces of data.

How do we solve these problems? In the next article we’ll look at some techniques to help refactor this code into something that’s test-drivable, flexible and robust. I also hope that you’ll find something to help you avoid this type of nightmare in the future.

Notes

I know the first test would not compile until adding the implementation code.
I know this login code is susceptible to SQL injection.
I know this login code is not even remotely secure.
I know that you should compare hashed passwords; not plain text passwords.
I know that the example gets a little long-winded. Thanks for making it this far.
There are a number of recent blog posts that have inspired this article. I’ll list them in the next part.

XP , ,

  1. Rob H
    November 20th, 2008 at 09:24 | #1

    Great - thanks for sharing this :-). Looking forward to the next.

  2. November 20th, 2008 at 09:35 | #2

    You raise some excellent points. Of course you didn't even get into the main problem that make TDD out of reach for most developers, especially Delphi developers: Their business logic code is tied directly to the UI!

    So instead of a TLogin object they just have

    Procedure Form1.Button1Click(Sender: TObject);
    var
    UserQuery : TADOQuery;
    DBUserPassword : string;
    begin
    UserQuery := TADOQuery.Create(nil);
    UserQuery.ConnectionString := 'Driver={SQL Native Client};' +
    'Server=localhost;' +
    'Database=SecurityDatabase;' +
    'Uid=myUsername;' +
    'Pwd=myPassword;';
    UserQuery.SQL.Text := 'SELECT Password FROM Users WHERE UserName = ' +
    QuotedStr(Edit1.Text);
    UserQuery.Open;
    DBUserPassword := UserQuery.FieldByName('Password').AsString;
    UserQuery.Free;
    if DBUserPassword = Edit2.Text then
    begin
    Form2.Show;
    Close;
    end
    else
    Label1.Caption := 'Invalid password!';
    end;

    Let's see you test that!

    Anyway, great article. I am looking forward to the next one.

  3. John E
    November 20th, 2008 at 10:21 | #3

    Great post… brittle tests is a major cause for TDD failure. I am looking forward to your next post.

  4. gverdile
    November 21st, 2008 at 07:35 | #4

    Great post: in a little post you give an excellent example of what happens when thee techniques are used but I think a better skill for developers could be the key. Timeline is a problem but bad or failure software in production is definitely worst!
    Thanks

  5. esteban
    November 22nd, 2008 at 09:48 | #5

    woooow ! great article. thank you very much Jody Dawkins

  6. February 12th, 2010 at 22:01 | #6

    Hi,
    Really informative post. This will really help me a great deal in starting my own business. Keep posting the good work.

  7. April 1st, 2010 at 10:56 | #7

    Such a nice article, thanks for sharing, it helped me so much!

  8. May 14th, 2010 at 03:10 | #8

    Thank for this article)))

  9. May 27th, 2010 at 03:40 | #9

    It is very important to finish the well done book reports essays paper or term papers just about this good topic to have the excellent mark at school.

  10. June 18th, 2010 at 17:02 | #10

    Thanks for the very helpful information.. I dont know much about this.! And im glad for your help..

  11. July 2nd, 2010 at 13:20 | #11

    Is jQuery a new programming language?

  12. July 9th, 2010 at 18:26 | #12

    This will really help me a great deal in starting my own business.

  13. July 15th, 2010 at 00:31 | #13

    I love your post ,thank you for sharing.

  14. July 28th, 2010 at 01:11 | #14

    This is a bit complicated, something that needs focus to be understood.

  15. July 28th, 2010 at 22:42 | #15

    I propose not to hold back until you earn enough amount of money to buy all you need! You should get the mortgage loans or just credit loan and feel free

  16. August 3rd, 2010 at 01:29 | #16

    boring. i have seen these styles before. not original at all.

  17. August 3rd, 2010 at 18:08 | #17

    boring. i have seen these styles before. not original at all.

  18. August 6th, 2010 at 03:49 | #18

    Nice effort, very informative, this will help me to complete my task.

  19. August 6th, 2010 at 07:14 | #19

    Some people require to write a lot to get the essence of the college paper writing. But if different students are pressed for time, this will be better to buy papers online. Then this would be available to save reputation.

  20. August 6th, 2010 at 21:10 | #20

    Japan Flowers
    I really liked your article. Keep up the good work.

  21. August 12th, 2010 at 12:34 | #21

    Our thesis writing service is the best but our firm thank you for the perfect note referring to this good topic. Thus, after that people have knowledge just about custom dissertation and purchase essay.

  22. August 13th, 2010 at 04:10 | #22

    Of course you didn’t even get into the main problem that make TDD out of reach for most developers, especially Delphi developers: Their business logic code is tied directly to the UI!

  23. August 17th, 2010 at 14:26 | #23

    Everybody are always asking just about the good old time. I reply, why do not you state the nice nowadays? I’ve have a lot of free time with research papers aid, and can do everything what I like!

  24. August 18th, 2010 at 01:59 | #24

    We sincerely got a kick out of your post. It appears like you have really put a good amount of effort into your post and this world need a lot more of these on the net these days. I don’t have much to say in retort, I only wanted to comment to reply well done.

  25. August 18th, 2010 at 02:27 | #25

    Your academic writing problems will not disappear if you stay performing nothing! Be elaborated in your grades improvement, contact custom research papers writing services.

  26. August 20th, 2010 at 10:10 | #26

    This is what I was searching for a long time! Many thanks for this subject just about college! Once someone pronounce that In union there is might. Our high trained crew can support you in writing custom research paper.

  27. August 20th, 2010 at 10:22 | #27

    Software that is testable has a far greater chance at being great software than code that is not testable.

  28. August 20th, 2010 at 14:04 | #28

    In all my practice with school programs, essay is the most professionally organized program I have meet.

  29. August 21st, 2010 at 03:21 | #29

    Wonderful and nice post about “Busting the great TDD myth”.

  30. August 21st, 2010 at 05:18 | #30

    I found your website perfect for my needs. It contains wonderful and helpful posts. I have read most of them and got a lot from them.

  31. August 22nd, 2010 at 12:30 | #31

    the nice nowadays? I’ve have a lot of free time with research papers aid, and can do everything what I like!

  32. August 23rd, 2010 at 05:41 | #32

    Great info.I like all your post.I will keep visiting this blog very often.

  33. August 25th, 2010 at 12:29 | #33

    By learning these technologies, you open up so much more possibilities than if you narrow yourself to a select few set of components.

  34. August 26th, 2010 at 22:56 | #34

    Thank you very much. I am wonderring if i can share your article in the bookmarks of society,Then more friends can talk about this problem.

  35. August 26th, 2010 at 22:57 | #35

    Classic exposition, I have also mentioned it in my blog article. But it is a pity that almost no friend discussed it with me. I am very happy to see your article.

  36. August 27th, 2010 at 09:18 | #36

    you open up so much more possibilities than if you narrow yourself to a select few set of components.

  37. September 1st, 2010 at 01:34 | #37

    I am more than happy to have come across your blog, it’s one of the bests, really love what you do, keep up the great work.

  38. September 1st, 2010 at 22:53 | #38

    That is not really easy to hear information referring to this postthus, essay writing services will propose to buy custom essay papers to receive right knowledge and it’s possible to have custom essays per really low prices!

  39. September 6th, 2010 at 17:30 | #39

    I will come back to look the best post

  40. September 7th, 2010 at 19:43 | #40

    I am more than happy to have come across your blog, it’s one of the bests, really love what you do, keep up the great work.

  1. December 1st, 2008 at 14:51 | #1
  2. May 3rd, 2010 at 15:32 | #2

*
To prove you're a person (not a spam script), type the security word shown in the picture. Click on the picture to hear an audio file of the word.
Click to hear an audio file of the anti-spam word