Busting the great TDD myth
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:
- Checked in the code
- Added the user into the test database
- 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.
Great - thanks for sharing this :-). Looking forward to the next.
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.
Great post… brittle tests is a major cause for TDD failure. I am looking forward to your next post.
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
woooow ! great article. thank you very much Jody Dawkins
Hi,
Really informative post. This will really help me a great deal in starting my own business. Keep posting the good work.
Such a nice article, thanks for sharing, it helped me so much!
Thank for this article)))
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.
Thanks for the very helpful information.. I dont know much about this.! And im glad for your help..
Is jQuery a new programming language?
This will really help me a great deal in starting my own business.
I love your post ,thank you for sharing.
This is a bit complicated, something that needs focus to be understood.
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
boring. i have seen these styles before. not original at all.
boring. i have seen these styles before. not original at all.
Nice effort, very informative, this will help me to complete my task.
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.
Japan Flowers
I really liked your article. Keep up the good work.
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.
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!
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!
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.
Your academic writing problems will not disappear if you stay performing nothing! Be elaborated in your grades improvement, contact custom research papers writing services.
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.
Software that is testable has a far greater chance at being great software than code that is not testable.
In all my practice with school programs, essay is the most professionally organized program I have meet.
Wonderful and nice post about “Busting the great TDD myth”.
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.
the nice nowadays? I’ve have a lot of free time with research papers aid, and can do everything what I like!
Great info.I like all your post.I will keep visiting this blog very often.
By learning these technologies, you open up so much more possibilities than if you narrow yourself to a select few set of components.
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.
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.
you open up so much more possibilities than if you narrow yourself to a select few set of components.
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.
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!
I will come back to look the best post
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.