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.

Advertisements

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.

On Unit Test Coverage and Code Complexity

A team lead approached me asking what should they define as minimum unit test coverage percentage and code complexity metrics. I had to think about my answer a bit and came up with following, which I think is worth sharing to others as well.

Unit-test Coverage %

The more good unit tests there is covering the code, the easier it will be to read, understand and change the code later on. Excellent answer to the question of what should the coverage be can be found from StackOverflow: http://stackoverflow.com/questions/90002/what-is-a-reasonable-code-coverage-for-unit-tests-and-why

If you still insist on having a target percentage, I’d personally recommend to aim for 100%, which is achievable if you use strict TDD. By strict I mean following the Red-Green-Refactor pattern with discipline. There’s a good presentation about this on InfoQ: http://www.infoq.com/presentations/TDD-as-if-You-Meant-It.

Being more reasonable, specially if you already existing code base, you might set the target to 70-80%. Keep in mind though not to stare at the numbers only! Bad tests also give you high coverage but not much confidence or easier refactoring. For this as well, there’s an excellent article available: http://www.sustainabletdd.com/2011/12/lies-damned-lies-and-code-coverage.html

Code Complexity

Code complexity  figure tells quite well how easy it is to read and change the code. It also affects unit testability as smaller methods are generally easier to unit test. I’d aim for cyclomatic complexity of less than 10 per method. More complex code usually contains more bugs as well, so if you find some parts of the code to be more complex, do focus your testing efforts on that area.

We have one project where we have set up our Jenkins job so that it becomes unstable if the amount of complex methods grows. This project now has average method complexity of 5.25. Still there is one methods with high complexity (23 in fact) and about 50 methods of medium level of complexity (10-20).

There might be a good reason for high complexity number e.g. an event handler with a long switch clause. While this might be avoided with polymorphism (see http://sourcemaking.com/refactoring/replace-conditional-with-polymorphism), but this might also make the code more difficult to maintain.

Summary

It’s good to early enough think about code maintainability and the speed of development in the future. Unit test coverage and code complexity are factors which affect those a lot. Other things worth investing to are code reviews as a standard practice and pair programming. Both work as fast feedback cycles to catch not only defects but also code which is difficult to maintain.

In the end it’s up to you and your Product Owner to decide how much do you invest in code quality. Code which is simple, produced with TDD and covered with good unit tests is cheaper to maintain in the future. On the other hand, if these factors are not taken in account, it’s like taking a loan from the bank. You’ll manage a long time by just paying the interest, or parts of that, but eventually you need to start paying back the debt as well. Or take another loan.

It’s more likely that you will produce Clean Code when you have proper measures in place to early detect code going bad. Also worth to note is that you should always aim for writing as less code as possible. A nice article on this subject: http://mikegrouchy.com/blog/2012/06/write-less-code.html