Let me quote my answer to the latest #FridayDevThoughts with the same topic:
Planning. A lot of it. And integration tests. Also a lot. Also, Iʼm not always utilise TDD, but when I find a bug, the first thing I do is writing a failing test before.
Grammar errors aside, my main weapon is testing. You can format your code whatever you like, if the logic doesn’t meet the requirements, nothing will help.
I try to make sure my code is modular enough that it can be unit tested at all.
Then I set up contracts between the caller and the callee: if function f() gets this and this kinds of arguments, it should return this type of value. In case of Python (my main language as of now) I document these in the function/method docstrings, which is continuously checked by PyLint, although it can’t enforce it.
As a next step, I write unit tests that check if the function fulfils that contract. This is the hard part, especially when I change the contract.
And finally, I write integration tests. I really don’t like it, because when I get to this point, I usually know by heart what the application should do, and writing them as code (be it Behave or pure Python) is a true burden. Still, not as bad as filling a 100-row questionnaire about our security solutions within the company…
Going back one step, I also plan a lot. According to my WakaTime profile I usually code only 13-15 hours a week. The rest is meetings (maybe 5 hours, 10 tops) and planning. This doesn’t mean my plans are always perfect, though; last week we argued on something with a colleague of mine for two days when I realised what he’s telling is a much better idea.
And when I implement a bad idea anyway, or simply add some bugs, my first step is to write a failing test on the integration side. During the time I debug the application to find the root of the problem, I also add new unit tests to functions that don’t have one yet (no, reaching 100% coverage is not that easy), so my code gets better and better every time.