Thursday, December 08, 2011

Test first != TDD

Recently I've seen a misunderstanding of TDD(test driven development), that is, if you always write test first and write code that passes that test, and voilĂ , you are TDDing, everything will be better now. That is not TDD. That is 100%-test-coverage-driven-development.

TDD is NOT about achieving 100% test coverage (which IMO is meaningless).

TDD is about using test to drive development.

More specifically, TDD requires three equally important types of activities:
  • writing test
  • writing the simplest code that passes the test
  • refactoring code that keeps tests passing
A common misunderstanding is that the refactoring step is optional. It's not. The following is not TDD.
First the test:
   describe 'alert' do
      it 'should log the message' do 
         logger.should_receive(:info).with('a message');
         subject.alert('a message');
      end
   end
Then the method in tested class:
    def alert(msg)
       logger.info(msg)
    end
Then you are done with this method. This is NOT TDD.
First, you didn't refactor the code. Second, it's not likely that the test will help when you need to refactor this code in the future. In one sentence, the refactor-and-keep-tests-green part is missing.
So what will a TDDer do here? Simply write the code without the test. A typical argument against it is that what if some other developer accidentally delete the logger.info(msg) code? Well, that's not the purpose of TDD, that is what version control is for.

In fact, it can be argued that this test is a form of code duplication. It's just that this duplication is very easy to detect (if you change one place without the other, the test will break.)
Things could be a bit different if your code is like this.
   describe 'alert' do
      it 'should log the title and message' do 
         logger.should_receive(:info).with('a title: a message');
         subject.alert('a title', 'a message');
      end
   end

   def alert(title, msg)
      logger.info(title + ': ' + msg)
   end
There is a possibility that you need to refactor to improve the performance of the string concatenation. In this case, the test here has some value.

So, the purpose of tests in TDD is to facilitate refactoring.

In many cases, tests give you the ability to refactor code with the confidence that it won't break functionality. In other cases, you may have to change some tests to do your refactoring, because unless it's a real black box end to end test, you are always going to have some form of logic duplication in your tests.
So a test is probably helpful for some refactoring but impeding for some other refactoring.

The value of a test is determined by the likelihood of it helping refactoring v.s. the likelihood of it impeding it.

When writing tests in TDD, we need to maximize this value with as less cost as possible. Tests on constructors, accessors, constants and simply delegations are typical examples of test with zero or negative value. Pure interaction tests are likely to have less value than a more end-to-end test but it should be evaluated on case-by-case basis.
You also need to take cost into consideration - for example, end-to-end test might takes more effort to write and/or more resource to run.
There is no silver bullet. Always-write-test-first isn't one. It takes some effort to determine when to write tests and how to do so, but it's not too hard - just focus on the value of the test, that is, how much benefits it can bring to immediate and foreseeable refactoring.

Update on Feb 11, 2012

I got the chance to discuss with Martin Fowler about this topic last night. The conversation started when I said "some people think TDD is all about write test first and implement it then done." Martin immediately replied "no, no, no, always refactor later. It should always be red, green, green." Then he quickly reaffirmed my understanding of TDD addressed above:
1, Achieving 100% of test coverage is NEVER the goal of TDD. A sound value of test coverage in TDD should be in the 90's.
2, Tests are for the sake of refactoring. They are written for testing "interesting" stuff. He would not test simple delegation (like the example given above) . Testing a simple delegation using mock has no value since it does not provide any benefits to future refactoring.
He also preferred test at a higher level which I couldn't agree with more. Recently I found that the higher level your tests are the more value they provides. Lower level unit tests are cheaper, but you need to keep in mind the value they can bring. Again, tests are for the purpose of confirming that some "interesting stuff" works, not that some code were written.