loading...

My personal take on TDD

grahamcox82 profile image Graham Cox ・2 min read

I'm going to start out saying something that will upset people. I don't personally think TDD works well for Unit Testing.

Now, don't get me wrong, TDD works fantastically for some people. And I personally find it works really well at a larger scale. But when it comes to writing Unit Tests, I find that writing the tests first just doesn't work as well.

This might be a side effect of my Java background (static typing, compiled language means that you can't write unit tests really until you've got something to test)

When it comes to Acceptance Testing however, I think that TDD is a great tool.

So, what's the difference?

At the Unit Testing level, you are generally not writing a single unit of code in one go. You will often find that you are writing several different pieces of code that all work together, and the design of these different pieces is fairly fluid whilst you are developing them. Writing the tests for these pieces of code is - for me - counterintuitive.

On the other hand, at the Acceptance Testing level you hopefully have a decent idea of what you are writing and how it is going to work. Whether this is the UI/UX, or the REST API, or whatever the external interface to all of this is, you should know how this part of it works before the tests are written. You should therefore know what you are testing and how it will work ahead of development, which means that you can write the tests for it first.

So, for me personally, TDD doesn't work well for Unit Testing.

Discussion

markdown guide
 

I am no TDD expert, but I find it useful in forcing me to think through the design of classes and how they should interact - before implementing it.
It is hard to write tests if you still don't have a clear idea how it will all work in the end, but I use that as an incentive to iterate on the design in my head or on paper until I actually do have some confidence of how the final design will be.

Sometimes I need to change things up and have to change the tests as a result, and this all adds overhead, but I usually end up appreciating a better design with TDD as opposed to when I just start coding with a sense of how the design should be and figure out things as I go.

Finally, I find it very tiresome to write tests after the implementation is done, since it's all working now and I want to start on The Next Thing. Looking back, I have very rarely written tests after completing the implementation first. With TDD it's kind of reverse, I get the satisfaction of seeing progress as I pass more and more tests - and it is almost bordering to fun. Almost.

 

the design of classes and how they should interact

That quote there is key to what I was saying. You are talking about class*es* plural, and the interactions between them. To me, that's an Integration Test and not a Unit Test.

Integration Tests I absolutely agree that Test-First TDD works great for. Write the tests for the feature as a whole, and then implement the individual units to make it work.

And I know full well that Test-First TDD works wonders at the Unit Test level for a lot of people. And that's fine. I'm just not one of them, and that's fine too.

 

What I meant to say, is that I use TDD for both unit testing and integration tests. Once the interaction of classes is defined in my design, then I know better what each class should be responsible for and can start unit testing those.

 

Totally agree. Feature/acceptance TDD (e.e. BDD) is essential and can drive your design as well as reduce bugs. Unit-test TDD on the other hand, presumes a level of implementation knowledge that's lacking in many development cases. Often, you don't know the API of a class until you actually write it. And then, even more often, you get to change that API multiple times, as you progress with writing the rest of the system.

Unfortunately, some people are blind followers of the cult of unit-test TDD and you can usually tell by the level of mocking and stubbing found in their test code. So, yes, well said!

 

I will say right now that I'm all for mocking in your unit tests. But that's because I'm all for Unit Tests being the test of a single Unit, instead of a whole. To my mind, the hierarchy goes:

  • Unit Test - Tests a single Unit (Class, Module, etc) in isolation. This will test that it calls it's dependants correctly, and that it produces the correct outputs from it's inputs
  • Integration Test - Tests a collection of Units all together. This might test that a controller, retriever, and DAO all work correctly in conjunction. There should be no mocks in here (With the possible exception of any calls to external systems)
  • Acceptance Test - Tests that the running system as a whole works. Instead of testing the controller, this will test by making HTTP calls to a running system, or by running a browser pointing at the webapp.
 

Wasn't arguing against mocking/stubbing, just the level of it in some codebases, which for me is a code smell. That's why I think it's sometimes better to write unit-tests only after you have all your dependencies and class APIs figured out.

 

I have run into a similar experience. There is code I write that I consider "exploratory code". You don't really know the API's that you will be calling or how to even solve the problem. You need to write some code and see how things react. To see if what you are doing is even possible. In this situation, I don't know what my test should look like.

I have also found that after writing my code, I will refactor it a bit. This tends to changes in the tests.

When I have done test first, I found myself deleting and rewriting a lot of unit tests after the code is complete. Sometimes entire classes and their tests were removed.

 

It's down to the fact unit testing is "white box testing" in disguise.
People don't consider the behaviour of the class, but its implementation.

Implementation code and test should be independent, in a sense an implementation must be able to be refactored while the test stays the same. There's no other way to reach the "red green refactor" mantra if you have to change your tests when you change the implementation. This is a sign of a very brittle test suite.

It's easy to spot this form of white box testing as opposed to black box, behavioural testing: it has method expectation, mocks, etc… with the idea of "covering every line of code because 100% coverage". A test should not be the implementation in reverse, but a description in terms of inputs and outputs.

 

This idea works really well when the units being tested are self contained. It falls apart when the units being tested are part of a bigger whole.

As an example here. Say I'm writing a user management system. Part of this entails the ability to load users from the database. As such, I've got:

  • User DAO
  • User Retriever ** Implementation of this that works directly in terms of the DAO ** Implementation of this that does caching
  • User Controller

Each of these should be tested to ensure that they work. But the implementation of the User DAO will directly affect the implementation of the Dao User Retriever.

An Integration test of this as a whole - from the controller to the database - makes a lot of sense to do ahead of times. You know what your API inputs and outputs are going to be, because they are part of your design. You know that when you call "GET /users/unknown" then you expect an HTTP 404, and when you call "GET /users/graham" then you expect an HTTP 200, and a JSON document in a specific format. You can write all of these first.

Unit Tests for the individual classes can be written ahead of time, but in my experience I've always ended up changing them as I write the code because of refectorings and tidying up and things like this. Little details like how the DaoUserRetriever works might change how the UserDao itself needs to work, which will change the unit tests for the UserDao.

And regarding the comment about mocks - if you don't use mocks then either you are writing everything as pure functions, or else you aren't doing Unit Testing. When testing that the DaoUserRetriever works correctly, you have to provide some implementation of the UserDao. Either the real one - which means instantly it's not a Unit Test - or else a Mock one.

 

"At the Unit Testing level, you are generally not writing a single unit of code in one go. You will often find that you are writing several different pieces of code that all work together.."
If you write several pieces of code that work together, the way to test them is by integration tests, not unit tests, don't you think?