Test-Driven Development for Dummies

Kuva
Flickr photo by julyYu. License: Attribution-ShareAlike 2.0 Generic, Some rights reserved.

If you thought this is a technical post you have mistaken. The purpose of this post is to try to explain what test-driven development (TDD) is so that anyone understands it. Or at least almost anyone, with at least some technical background.

Let me start by explaining what TDD is not:

  • It is not one form of software testing nor does it remove the need for that
  • It is not done in order to find defects from the code
  • It does not guarantee that there are no defects in the code
  • It’s primary purpose is not in trying to minimize amount of defects in the code
  • It does not magically solve all your problems

Instead, TDD is primarily a design method which aims in producing least amount of code which satisfies the software requirements. As an added benefit, TDD also ensures that code does what the person who wrote it intended it to do. Naturally, in case the software requirements, use cases or user stories are not right, the code might still not be doing what the user would like it to do, but this is another story and finding this kind of faults is not in scope of TDD.

So, yeah but, no but, yeah but, no but WHAT and HOW?

To begin with starting to design or implement any piece of software, developers needs to have requirements. They can be in any form popular at that time, use cases or user stories being ones often used at the moment. From a requirement, software development team plans the tasks which need to be implemented in order to fulfill the requirement.

Let’s have some example tasks:

  1. “Ask user’s name”
  2. “Ask user’s birthday”
  3. “Show a friendly greeting to the user”
  4. “Show a birthday greeting to the user”

Let’s start implementing those tasks using TDD.

First task, “Ask user’s name”. For this we probably need a method which asks for user’s name and stores it for later use. To begin, we will write a simple test in (pseudocode):

"User name should be stored after it has been entered":
    user_john = new User()
    user_john.set_name("John Doe")
    assert user_john.get_name() equals "John Doe"

This imaginary test doesn’t bother with how the name is asked from the user, it focuses in the storage of the gotten value and asserts that the value given is indeed stored for later use. When this test is run, it will fail as there is no other code written yet. In fact, this code would not even compile.

When a test is failing, the testing result is said to be red. This is always the starting point for adding the actual functionality. The reason for this step is to ensure that the test itself is not accidentally passing when it should fail.

Next step is to write the code to make the test pass:

class User:
    variable username

    method get_name():
        return "John Doe"

    method set_name(variable name):
        return

Now the code compiles and the test passes. Still this doesn’t look like it is ready to production yet, or does it? Anyway this state of TDD cycle is called green. Only at this stage, it is allowed to refactor the code. Refactoring means changing the structure of the code without altering any functionality. It is done in order to make the code cleaner e.g. by removing any unnecessary duplication, magic strings and numbers etc. It is a necessary step to be done during development although in this example of ours there is not much to refactor. After refactorings have been done, all the tests are run again in order to make sure none of the functionality was altered or broken.

As we see, our code is not yet ready so we need to do some work with it. The TDD cycle begins from the beginning, by writing another failing test or extending an existing one. We now choose the latter alternative:

"User name should be stored after it has been entered":
    user_john = new User()
    user_jane = new User()
    user_john.set_name("John Doe")
    user_jane.set_name("Jane Doe")
    assert user_john.get_name() equals "John Doe"
    assert user_jane.get_name() equals "Jane Doe"

Now when we kick off the tests run, it will end up in failing, red state. This means we can hop on to write some functionality again:

class User:
    variable username

    method get_name():
        return username

    method set_name(variable name):
        username = name

Behold, our tests are turning green again. After refactoring we would hop on to write first failing test for asking the birthday and so on and so on.

The TDD cycle is often called Red-Green-Refactor and it is essential that it is followed strictly. Only this ensures that developer writes as little production code as possible and that the code written does what it is intended to do. Refactoring step makes the code more clean, helping in maintaining that code later on. Good tests written during development also work as a splendid documentation of the code. A new developer getting familiar with code written using TDD will easily see what the developer who wrote the code intended it to do not to mention that such code is usually also easier to understand as it is cleaner. Good to remember here also that less code = less defects. So even though the main purpose of TDD is not to try to minimize the defects in the code, it sure does help in that.

If one follows the TDD discipline, the produced code will get 100% unit test coverage. This is something which is really hard, if not impossible to retro-fit in the code later on. Unit test coverage itself doesn’t make the code any better than code without any coverage, but it makes it easier, faster and more safe to change the code later. I claim that an average developer spends at most 10% of her time writing new code and rest of the time goes reading and changing old code. Developers in general do spend a lot more time reading code than writing it. So if we can make the code easier to read, it will save a lot of money in form of saved development time.

Unit Testing Is Boring

Unit testing is really really boring. It is waste of time and productivity, writing unit-tests is hard, takes long and might require time-taking refactorings to the code for it to be unit-testable. Regression testing is anyway a lot more effective in finding mistakes. Besides, unit testing provides very little value as it doesn’t tell anything about the quality of the software. Adding unit tests to code which has already been proven working by functional testing is just stupid.

That may sound a bit exaggerated, but I’m almost serious here. There are times when adding unit tests is really waste of time. There’s a lot of code which is hard or near impossible to unit test properly. But the last part about code being proven working by functional testing is in my opinion bullshit. Functional testing tests functionality and can prove that desired functionality has been achieved, but it cannot prove that the code does what the developer intended it to do. And no matter how much functional tests you add on top of your software, it doesn’t make the code any more maintainable.

Let me tell you  how I define testing and unit testing.

Testing gives quality information about software. Qualities being e.g. functionality, usability, reliability, performance etc. We do testing in order to determine the current quality level of a certain software. This information can be further used to make decisions e.g. whether to release the software now or later (or never which is a brilliant decision in some cases).

Unit testing does not give you any quality information about the software or it’s functionality, but it does give you information about the  quality the code. There is a difference. Unit testing, if done properly, can prove that the code does exactly what the developer intended it to do. Of course there might be mistakes made when deciding what the code should do, but that’s a different story. Unit testing also makes code easier to maintain and further develop.

To be able to unit test code, the code needs to be unit testable. This usually also means the code is cleaner (clean as in Clean Code) which makes it easier to understand and change the code. And when the code is covered with good set of unit tests, it is also safe to change the code. Every code written has a price. There’s a price to develop the code, but there’s (often even larger) price to maintain the code. Clean code which is covered with unit tests has a smaller price tag than code which is not that clean and/or is not covered with unit tests.

Adding unit tests to code is boring. This is true. And if the code will not be further developed, it is complete waste of time as well. Unit tests do not add much (or any) value when written afterwards, specially if long time has passed since the code to be tested was written. As said, adding unit tests to code which was not written to be unit testable in the first place is laborious and often requires big refactorings to be made before unit testing can take place. And as there are no unit tests to cover poor developer’s ass, those refactorings might break things really badly. Luckily there are good old books to help in tasks like this. Do read Martin Fowler’s Refactoring and Michael C. Feather’s Working Effectively with Legacy Code.

So, this far we’ve learned that unit tests often don’t add any value, are difficult to write and the whole thing is also really boring. Forget unit tests completely? No, I’ve found a nice solution which you’ve probably heard earlier as well. It’s called test-first programming or test-driven development (TDD). I personally like most the term red/green/refactor. After I learned to do TDD (as if I meant it) I’ve enjoyed writing code a lot more. Nowadays I don’t often get to write code, but when I do, I use TDD and I love it.

And even if you didn’t like TDD or don’t want to try it (if this is so, I’d like to know why), you can still add the unit tests while you’re writing your code. Then you must think about unit testability before you write the first line of production code. Code design must be done so that adding unit tests is easy.

As a final note, consider Martin Fowler’s words and think about this the next time you’re writing code:

Whenever you are tempted to type something into a print statement or a debugger expression, write it as a test instead.

Test Driven Sprint Planning – Retrospect

A while ago I blogged about trying out a different method for sprint planning.

The sprint went well, but unfortunately I had to leave that team so I couldn’t continue my experiment for longer than one sprint. I gathered some feedback from the team members to get some idea about whether the experiment was in any way good and if it’s worth to continue it at some point.

Here are the results:


The method of planning tasks as tests-based during sprint planning session:
  • Most team members thought that it wasn’t more complex than usual way of planning
  • One team member thought that it was simpler than the usual way of planning
Benefits for the sprint planning part:
  • Most team members thought it provokes thinking about how specific tasks will be tested
  • One team member thought it makes it easier to find all tasks related to a certain story
  • One team member thought that the tasks also work as acceptance criteria
Sprint execution:
  • Half of the team members had to add more tasks during the sprint, because:
    • wanted to make sure that some minor feature details were working ok
    • the tasks in a story were too little to cover it
Task granularity:
  • Most team members thought the task granularity was good
  • One team member thought the tasks were too big to be able to use them as unit-tests but too small for the story to get fulfilled. This team member wasn’t present in the planning.
Tasks as basis for the  list of unit-tests:
  • Most team members used the tasks as basis for their list of unit-tests
  • One team member couldn’t use the tasks as basis for the list of unit tests. This person wasn’t present in the planning.

So, what did we learn from this experiment? To me this looks promising, there was no single person in the team who thought this was rubbish and shouldn’t be tried again. On the other hand, there were few team members who weren’t part of the planning session and the method wasn’t explained to them in more detail. As a result one team member thought we were trying ATDD, which wasn’t the case. Several people said one sprint is too short time to make any good conclusions and I fully agree.

Regardless of this, I have heard the team still does the sprint planning using traditional methods.