6 Misconceptions about TDD – Part 4. There is one right granularity of steps
Learn what the Baby Steps approach in TDD means and discover if it is always the only way to develop software products in the spirit of Test-Driven development.
When using TDD one might wonder “how granular my tests should be”. Is it better to have fine-grained unit tests so I can tell if every object (or class if you prefer) is doing the job I expect it to do or is it better to have more coarse-grained tests so I have more freedom to change the implementation without breaking the tests?
There is an idea of “baby steps” among TDD practitioners. Baby steps method is a TDD approach that you make the smallest changes possible – for both test and production code. The routine more or less looks like this:
- Write a small failing unit test
- Write the simplest production code which will satisfy the test
- Refactor (either test or production code)
- Go to 1.
The crucial part for baby steps is 2., you want this step to be as quick as possible. Sometimes it means you just want to hardcode the expected value and return it, write the second test to ensure different testing data and satisfy the test with the simplest solution.
This process leads to a great number of test cases and code changes. What we get is the quick feedback of the code we write and most likely quite good confidence in our codebase. Poor effectiveness is the drawback – we need to write and change a lot of code to complete the feature.
The contrary approach would be to write high-level requirements as test cases for our SUT and add production code in bigger chunks. Let’s call this leap approach. The benefit would be effectiveness burst and possible lower level of test to production code coupling in the exchange for having slower feedback and possible smaller confidence in the codebase.
The confidence factor
In the previous section, I mentioned granularity and its influence on the confidence in the codebase. Let me explain what I had in mind. Let’s say you have feature X to implement. Assuming you’re doing TDD how quickly you can get from first failing test to the finished feature implementation can rely on a couple of factors:
- How complex is feature X
- How well you understand the feature
- How big the influence it has on the rest of the system
When asking yourself such questions many doubts may arise. You might want to mitigate the risk of implementing the feature incorrectly by taking small implementation steps – write small chunks of code and verify if they meet requirements you form along the way.
You may revalidate requirements multiple times when working. This play along with baby steps approach nicely – you write test, write the simplest implementation, refactor and validate your understanding of the feature before writing a new test or changing some existing one. You constantly put your understanding in question and thus make yourself confident that you’re doing the right job.
Of course in every point of such routine you want to have at most one failing test at the time, this also gives you confidence that you can quickly rollback to previously “working” version of the code.
That said having many test cases and implement the feature by many careful tiny steps should increase your confidence in the codebase. I believe this is a huge gain but there are of course situations when you want to make bigger, more courage steps to achieve better efficiency. It is always about finding a good balance.
When starting implementing object behavior you need to make some decisions at the beginning:
- What will be my first test
- How should I make this test green
Answers to these two questions will affect how implementation will go on and how the final design will be.
If you choose the baby steps approach you will probably end up with more abstractions in your implementation. This is because you will change directions of implementation and adjust solutions more often. After every next step, you will have a moment to think about generalization and other edge cases. Also, you will try to model your problem more carefully. This workflow favors finding abstractions and make the model more complete.
On the other hand, if you choose a leap approach (bigger steps), you will more probably focus on making the code working end to end so the focus could be less on the insides. This could favor simpler design and fewer abstractions.
This isn’t a bad thing though, for simpler domains you will benefit from simple design and having architecture simple enough to deliver features quicker. Also, it is more probable to have bigger trust in simpler architecture than the more complex one – this will enable you to keep on going with the leap approach.
There are two conclusions I find true:
- The size of steps is often related to the confidence we have in the codebase and our understanding of the domain
- The size of steps in TDD can influence the final design we implement
These conclusions imply there are is no one correct size of steps in TDD. We should take factors like confidence in the codebase, domain knowledge, design, architecture and probably other factors into account.
The agenda of the article series “6 Misconceptions about TDD” is the following:
- TDD brings little business value and isn’t worth it
- We all understand the key laws of TDD in the same way
- TDD cycle can be neglected
- There is one right granularity of steps in TDD
- Mocks, mocks everywhere! There is nothing more
- Tests loosely coupled with code are reliable