What does TDD achieve?
If you are pair programming, TDD is excellent because it keeps the pair communicating on the same level. If you have an idea then you have to represent it as a test and the other person can follow your idea more easily.
Should you always write a test first? I think the answer is no.
So when should you write a test?
Tests help with design, so if you are still designing your code then you will benefit from writing your test first.
Tests help with refactoring. You don’t know when you will next need to refactor, but it is always better to have code that is easy to refactor.
Therefore you should not miss too many chances to put in a test harness. It will be better in the long run.
When should you remove a test? After the design phase a test is operating more as a harness. If the harness is too rigid then you might want to think about removing it, or modifying it.
What is hard about TDD?
When that feeling builds up that your test harness needs some attention, sometimes you just want to ‘get on and code’. It requires discipline to take a break from ‘producing code’ to spend time on ‘protecting code’.
When building a program I might:
- have a good hard think about it with a pencil and paper,
- dive right in,
- have a fiddle within a REPL.
The point of TDD is that it gives structure to the ‘dive right in and have a fiddle’ modes. It makes your fiddling more structured, if you like.
Coding, TDD, edge cases and the solution domain
You often find that your code works for most cases but not all. Mathematically we could say that you have defined a valid program on a subset of the whole domain of inputs/effects/actions.
Your tests are some way of defining your solution domain. If someone changes your code and one of your tests break then you could say that they have reduced the solution domain (and accordingly need to decide whether that is a bad thing which needs repairing).
The point of TDD is to allow you to ‘discover’ your minimally-viable-solution-domain by working from the outside in. For example, why spend much more time thinking about whether I need to ‘properly’ handle the edge case XYZ generally when it just might happen that I never need it other than in the specific case I have.
Refactoring and the solution domain
Refactoring is often about spotting patterns — removing boilerplate code, or modifying a section of code to produce the same result in a more efficient way.
The more you can appeal to ‘natural patterns’ or ‘natural assumptions’, the more readable you might hope your code to be.
The solution domain is similar. For example, L1 is the collection of integrable functions, and it is nice if you can make your theorem work on the whole of that domain. Imagine how irritating it would be if your theorem worked only for functions in L1 which do not hit 10 more then 13 times. Naturally you would try to work out whether this edge case is real or just a byproduct of your approach.
Something similar happens in coding, but we have much less time to think about our approach — we have to make many more decisions and we have get the product shippped.