Not directly tied to the question, but I think TDD in big projects finally clicked for me when I put together the refactor part with how you move the tests and make them work in the context of the code.
YOU DO NOT PUT ALL YOUR TESTS AT THE TOP
This seems obvious now, but god was it stuck in my mind that you'd have like 500 tests all trying to test through your logic 6 layers deep. I also thought that if you did move them, your tests would stop reflecting business requirements, and just turn into functional tests, until I ran it through.
Say you have a request for a WeatherForecast API from the business, it has these requirements:
- Return humidity, temperature and percipitation
- Return weather class, e.g. cloudy, rainy, etc.
- Location based
- Go up to seven days in advance
Modelling this out, you might have the following layers of code:
- Handlers
- WeatherProvider
- WeatherForecast
- WeatherClassifier
- Location
Now you would start with the WeatherForecast, and add your first few tests to get the basic weather data. Get them running, refactor and you're starting strong.
Carry on and add the weather classifier tests in, get them passing, and refactor, part of that means to move the classifier to it's own interface. Now why do the classifier tests stay with WeatherForecast? This is the important part. They don't. Move them to the Classifier layer. Update them if you need to to match the new layer.
Rinse, repeat for all layers.
This way if you end up with a bug that only occurs when the humidity, temperature and percipitation are certain levels, causing the classifier to go haywire, it's simple to add a new test in the classifier, create the test with the scenario, you don't have to fall through all the layers, and the tests still map to business requirements.
Now you may be thinking 'Well where are my handler tests?' and this is a good observation, do you have any requirements given on the handler and how it should behave? No? Then you don't need to test it, or write any code, and no handler is required.
Obviously that's not the case, but it highlights how TDD forces you to get good requirements, you would like requirements for your handlers on what to do if you get an error, what the response schema looks like, what format should they return data in, you might decide this yourself, but they're behavioural requirements, and you should treat them like so.
In the end, you have all your layers with their own tests describing the behaviour of that layer.
Your handler returns the weather data in json
Your WeatherForecast returns humidity, temperature, percipitation and weather class
Your WeatherClassifier calculates the type of weather based on the humidity, temperature and percipitation
And that's the beauty of TDD.