Terrence Joe bio photo

Terrence Joe

Agile Consultant

Email Twitter LinkedIn

Overview

TDD Marmite - love it or hate it

Test Driven Development has always had the ability to provoke a bit of a reaction out of developers. When you bring up the subject of TDD, it seems like there is one camp that loves it and believes its the only way, and another camp that hates it, calling it a cumbersome and unnecessary overhead.

Tom instantly regretted saying he didn't really like TDD

As time progressed, the pro TDD camp seemed to grow from strength to strength, and pretty soon we had a community who all had a fairly uniform way of practising TDD. It became almost standard fare to follow Red (write a failing unit test), Green (implement your method to make it pass), Refactor (clean and optimise your code), then continue with the next method.

We had at least one test per method, and to keep our tests small and isolated we made widespread use of Mocks throughout our tests.

It was all going well, our code was well covered, we had a warm fuzzy feeling. This was the right way to TDD, if someone didn’t do it this way then they weren’t doing it right.

A fly in the TDD ointment

One of the selling points of TDD - was that you could in theory refactor your code, and the tests would make sure that it still worked as designed.

Only - it never worked that way. Anyone who has done TDD like this with lots of mocks will have the experience of refactoring their code, then having dozens of broken unit tests that you have to fix up before they will even compile and run.

This is a problem. Firstly, when refactoring you should change only your tests OR your code, but ideally not both at the same time. This helps to ensure that your code changes are not breaking the intended behaviour.

Secondly - having this happen makes your tests a serious barrier to refactoring your code. When having lots of small unit tests with Mocks everywhere, your tests are very brittle, and having to rewrite them everytime you refactor your code will be a serious time hit.

Slowly, bit by bit, it seemed as though TDD would die a slow and painful death.

TDD - is there another way?

“My personal practise is I mock almost nothing” - Kent Beck, creator and one of the greatest authorities on TDD

And Martin Fowler followed a similar sentiment in their Is TDD Dead debate. This may have been somewhat shocking for someone who was living in the happy TDD bubble, with mocks scattered all through their tests.

But you cannot possibly ignore a statement like that from such respected sources. So - how do you write unit tests without Mocks?

TDD - there is another way!

The popular style of TDD described above is brittle, because the tests are at a very low level. Known as the “Mockist” style, the definition of a “Unit” in Unit Tests is down to a class; we need a new class, so we add some new unit tests. Testing this way means we are essentially testing the implementation details of our code.

The alternative? Is closer to the “Classicist” TDD style, where the trigger for writing a new test should instead be when we have a new behaviour requirement. Our flow should be Red, write a test for a new behaviour, Green, write the dirtiest, monolithic method to make it pass, then Refactor the code to make it production worthy. In the refactor part we will likely introduce new methods and classes, but, and this is important, here we are not be required to write tests. All of this implementation is covered by our initial test on the new behaviour.

This means we will likely end up with far fewer unit tests, with almost no need to mock anything. And importantly, we should be able to refactor our code and change the implementation details as we see fit and still be effectively covered by our original tests. Result!

But that’s just BDD

Behaviour Driven Development you say? That would be correct. Many people implement behaviour tests or acceptance tests at a higher level, and then have unit tests at the implementation detail level. If we practise TDD as described above however, we can essentially kill two birds with one stone.

Note: I am not saying we should only have unit tests; there is still value in having higher level tests, such as integration tests.

A non-technical example

Lets use the example of wanting to test that a car will move forward. The two approaches would look like this.

The Classicist approach

We have one new behaviour; when we move the accelerator down, the car moves forward. We write one test at this level.

The Mockist approach

We might have the following tests:

  • When the accelerator moves down, the it sends a “down” message to the throttle valve
  • The the throttle valve receives a “down” message, it opens the air vents
  • When the throttle valve opens up, more fuel is sent to the engine
  • When the engine rev’s go up, and the car is in Drive, the clutch is engaged
  • When the clutch is engaged, the car moves forward

Comparison

In this somewhat contrived example, you can see that the Mockist approach is at a lower level of testing. The tests are so small, they know all about the implementation details of the cars mechanism to move forward. The problem is that if you wanted to change how the car works, say upgrade the fuel injection system, then the tests will likely need to be changed as well because they have intimate knowledge of all of these moving parts.

The classicist approach however, being at a higher level, does not care about the lower level implementation. It has just one test on the desired behaviour, and all it knows is that when you put the accelerator down, the car should move forward. The benefit is that you can improve the inner workings of the car, and the original test will still ensure that this behaviour works as intended without having to be changed.

The challenge is getting your unit tests at the right behaviour level. You don’t want them too small, where they know implementation details. You don’t want them too high, at a UI level for example. The right level is usually at the public API level, the point at which your UI/System events drive and use your business logic if you are using the Ports and Adapters architecture for example.

If you are still cynical

Don’t worry, its normal. I was too when I had first heard of following TDD like this. In fact if I was interviewing someone and they told me this in an interview, I likely would have grimaced and questioned how well they knew TDD.

It wasn’t until I had read a bit more about it and tried it, that I really felt the benefits of having tests that don’t know too much about your implementation details. Being able to refactor your code, not have to touch any tests, and still know that your program is tested and working - is quite frankly liberating.

I plan to write a lot more on TDD, and automated test strategies in future, so stay tuned for more later.

Summary

I suppose the main thing we should take away from this though, is that there is never just one and only correct way of doing anything, including TDD. We can use lots of Mocks, or we can unit test at a slightly higher level. There is never a black and white, wrong and right answer; we can only try different methods and see which ones work best for our situation.

In my case, I have really seen the benefits of moving my unit tests up to the behaviour level. However, I would never dictate one way or the other; this is very much a team decision, and while teams should certainly try different approaches, they should always do what works best for them.