Writing a test before you write the first line of production code is a revolutionary concept. No, really. Blew my mind when I first heard about it. Running a test when you sure know it won't pass feels like programming Mad Hatter style. When you start doing it, you feel like a rebel, defying all dogmas and dusty stereotypes.
But when you finally fall asleep, a grinning deity comes to your bed and whispers these bitter words in your ear:
You're not doing TDD
This is called Test First development. TDD is much more than just doing it backwards.
Let's reiterate the process of Test First Development. First, you write a test for a feature and watch it fail. Then you implement the feature and check if the test passes. If yes, move on; if not, tweak the implementation until it does. Essentially, it's a micro-waterfall approach. The confusion comes from simple TDD examples like Calculator -- the simplest code that works is actually the final solution. In real life, however, a feature implementation can be quite complex.
Isn't it TDD?
One of the biggest changes that TDD brings to the table is that you build your feature incrementally. You're not making a big jump from nothing to a Big Program, The idea is that you write the simplest code that works.
Say, you want to implement this wonderful feature: a logged in user should see her name in big bold letters. Let's rewrite it so that we can write a test for it:
If a user's login is "neo", and the user is logged in, the UserNameLabel's text should be "neo".
(For simplicity I assume that we deal with either WinForms or WebForms). So, the test might look like this:
var text = GetCurrentScreen().FindUserNameLabel().Text;
The Test First approach would be to write the test, then write a fully blown membership system (assuming we cannot use an existing one), and then run the test again, hoping it turns green.
Going the Test Driven way
Now, let's do it the Test Driven way. What's the simplest way to make the test pass? No, not that one, there's a simpler option. Right, the first step is you just hardcode the label's text. This is another psychological block to overcome: force yourself to write an obviously wrong code. We can only promise ourselves that we'll make it better in a minute. It helps.
Now it's time to remember the TDD mantra, "Red-Green-Refactor". We've got a Green, it's time to Refactor. We should get rid of that hardcoded value. Again, we're not writing anything complicated, we start with a simplest thing that works, then modifying it to something more acceptable, doing one small step each time, running our test after each step. Most probably, the first step would not be getting rid of our constant, but moving it to a different place instead. The actual solution depends on what particular framework we use here. For example, we could add the following code to the UserNameLabel's Init handler:
UserNameLabel.Text = _membership.GetCurrentUser().UserName;
It remains to implement _membership.GetCurrentUser(), which is one step towards the success. Of course, we can (and should) write a test for this method as well. The first implementation would contain the hardcoded value as well, but eventually we'll get rid of it.
After removing the hardcoded "neo", thus finishing our R-G-R cycle, we probably end up with the following behavior: the screen always displays whatever a user has entered in the "Username" field, regardless of the password. This is another core principle of TDD: YAGNI (which stands for You Ain't Gonna Need It). There's nothing in our requirements that says what the system should do if a user enters a wrong password, so we are free to leave our implementation. After we receive another requirement which deals with entering invalid credentials, we'll write another test, and modify our code so that it satisfies both tests.
Which way is better?
Looking back at our list of virtues, we see the main difference: the Test First approach will not help us in designing our system. Given that this is considered the main benefit of TDD, failing to understand this distinction essentially means that TDD has failed here, leaving us with the additional burden of supporting the ineffective tests.
Wait, but this is an integration test you're talking about!
So what? Despite a common misunderstanding, TDD is not just about unit testing. I'm writing more on unit vs integration testing in the next post. However, everything written here can be applied to unit testing as well.