This article is going to be somewhat controversial, so I apologise for that ahead of time.
I don't like Test First Development. I love Test Driven Development, but for me it only works well in certain ways.
Firstly some definitions of what I'm talking about:
- Test Driven Development. This is the situation where you write tests for all of your code as you go, both to ensure that what you've done works and to ensure that it doesn't break in the future
- Test First Development. This is the situation where you write the tests for your code before the code itself. Proponents of this like it because it helps to nail down the way that the code will be used before you've even written it, which can be invaluable as part of the software design.
Now, to clarify, it's only certain forms of testing that I personally do not like Test First Development. Specifically, Unit Testing.
Test First Development works fantastically when you are developing an entire system that other code - either your own or other peoples - will depend on. This might be a REST API, or an internal module, or anything. The key here is that it is a whole set of code, and that it has to have a good external API because other code will depend on it.
For me personally, Test First Development doesn't work well when doing Unit Tests, because at the time of doing Unit Tests you are still not sure exactly what the Internal architecture of your code is going to be. Couple this with my preference for static typed languages and it gets to be a lot more trouble than it's worth.
In the world of Test First Development you are meant to:
- Write a failing test describing the next micro-feature you are implementing, or else refactor an existing test to cover it
- Write just enough code to make the tests pass
- Refactor your code
- Repeat
The idea is that the software design ends up being iterative and you don't need to spend a long time up front doing costly software designs.
In reality though, my personal experience of this ends up being.
- Write a failing test describing the next micro-feature you are implementing, or else refactor an existing test to cover it
- Decide if the code you've written so far will actually fit into the next microfeature or if it needs refactoring
- Refactor the existing code to better fit. You need to delete the new non-compiling test so that the other tests can run to tell if things are broken
- Re-write the new test
- Write the code for the new feature
- Refactor your code
- Repeat
The extra steps in the middle of all of this are very costly, and so in reality what you end up doing is closer to:
- Doing a full design for the entire (sub)system on paper first. Be it UML or just some notes on a napkin.
- Writing the Unit tests. These will fail to compile, so the entire build is now broken, and you don't know what other tests are still passing or failing
- Writing all of the code that you designed at step 1
Now, this isn't how Test First Development is meant to work. It's pretty much the opposite of how it's meant to work. But in my experience - which is admittedly coloured by the languages that I use - that's how it ends up.
On the other hand though, for anything that has an external API - be it REST, GraphQL, Java or whatever - then Test First Development is fantastic. In this case, writing the tests will help nail down the way that the API works, and will help to highlight places that the API doesn't work as well as you'd expect. You then end up with a better API before you've written a single line of Production code. You then get to build your individual units inside of this API confident that the external facing portion will be good. This is what Test First Development is all about.
Top comments (4)
I fail to see the necessity for steps 2 to 5 in your process? Why do you have to decide if the code you have written in step 2 needs refactoring, if you haven't written any code between step 1 and 2? The test is supposed to fail in the first step before you go about writing code to make the test pass. You would then need to run your test suite to identify and fix any unwanted side effects.
It is also important, in my opinion to correctly design the interface of the units in your system, because it facilitates teamwork.
Given that it's an iterative process, I'm referring to subsequent times round the loop.
Without doing detailed up front design of every class in your system, it's likely that you will write a class in one iteration and then change it in a future iteration. Possibly quite drastically.
The biggest problem I've got here is doing this in a compiled language. As soon as you write a new test for an as-yet-unwritten unit, your entire build fails to compile. This can make it unnecessarily difficult to determine what has and hasn't broken by the changes you're making in order to make the new test pass.
If instead you write the Unit Test - and I do only mean Unit Tests here - afterwards then you know that your code all compiles and that all existing tests still pass. You know the interface to your new code. You can test it and make sure it works, and if it fails or if the interface isn't as good as you first thought it's still easy to tweak.
Of course, the single most important thing here is - this is just my opinion. Everyone is different. There is no single perfect way to do this. If writing all of the tests first works for you then do not stop doing that. Do whatever works best for you to create great things :-)
Just curious what language and tools are you using? I have never ran into these issues using TDD. I use Visual Studio and it tells me right away what the compiler errors are. And if I am missing a class or function I just use the refactoring tools to create or rename them at will. Sounds a lot of issues might be tool related.
Uncle Bob, one of the biggest promoters of Test Driven Developement, wrote up a really good blog post about the problems you encountered here: blog.cleancoder.com/uncle-bob/2017...
The point people miss with TDD you need to do some kind of upfront design, then use the tests and code to validate your assuptions. You also need to design your tests to avoid all the extra work of reworking your tests when you refactor something. Using an interface or api layer between your code and tests allows for refacroring the code and not have to constantly rewrite tests. Also important to keep the tests just as clean as the production code.