It’s not really a lot of extra work to do that once you get into the habit. Tools like `git add -p` to stage partial changes in files can be useful for separating self-contained refactorings from behaviour changes if you realise they’re happening at the same time and starting to get tangled.
I mean, it sounds like an ideal one but I’ve never lived in such a world. Tests always seem to couple to the way code is currently organized, and I can barely imagine any refactoring change that wouldn’t require some changes in my tests.
Your public interface to your lower level modules/classes are the only things you need to directly test. If that's your smallest unit for testing, then a refactor should also (again, in theory) not impact them because the refactor should not change that public interface, at most changing the internal (private) functions and structure which the existing tests can verify just as they verified the unit before.
If you restructure your units (split a unit into two, for instance) then you'll have to change your unit tests and possibly some integration tests if you instantiated the previous unit directly as part of the test. But that's pushing the definition of a refactor.
- This works best if you actually embrace BDD to test for behavior rather than results. The behavior is less likely to change, and along with points below so too are the interfaces under test.
- I find the least test change in statically typed languages where defining types is the first “test”. Behavioral interfaces become much more stable as you define them if you actually know what they are upfront.
- Both of the terms “integration test” and “unit test” discourage this. Validating what your program actually does usually sits somewhere in the middle, and allows you to encapsulate a lot more of what would otherwise likely change in tests.
- Stricter interfaces fare better than looser. That doesn’t mean give up on the Robustness Principle, but it does mean don’t let it become infectious (I now want to blog about the “color” of functions which accept “options” or other miscellany). The sooner you parse/validate/have concrete and reliable types, the less your code under test and the tests themselves will change.
And that, while it’s my last point, is the most important: narrow types early. Get stability inside the seams of your program.
Magit is great for this: it shows an interactive version of `git status` (the "status buffer"), with diffs that can be expanded/contracted for each file and for each hunk; we can also selectively stage only a few lines from a hunk.
The other choice is to roll the whole thing back or practice forward-fixing. The latter is an entire discipline in itself.