Avoiding TDD making big refactorings harder [closed]
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 5 years ago.
Improve this questionI'm still relatively a beginner in TDD, and I often end up into the trap where I've designed myself into a corner at some point when trying to add a new piece of functionality.
Mostly it means that the API that grew out of the, say, first 10 requirements, doesn't scale when adding the next requirement, and I realize I have to do a big redesign on the existing functionality, including the structure, to add something the new stuff in a nice way.
This is fine, except in this case the API would then change, so all the initial tests would then have to change. This is usually a bigger thing than just renaming methods.
I guess my question is twofold: How should I have avoided getting into this posit开发者_Python百科ion in the first place, and given that I get into it, what are safe patterns of refactoring the tests and allowing new functionality with a new API to grow?
Edit: Lots of great answers, will experiment will several techniques. Marked as solution the answer I felt was most helpful.
How should I have avoided getting into this position in the first place
The most general rule is: write tests with such refactorings in mind. In particular:
The tests should use helper methods whenever they construct anything API-specific (i.e. example objects.) This way you have only one place to change if the construction changes (e.g. after adding mandatory fields to the constructed object)
Same goes for checking the output of the API
Tests should be constructed as "diff from default", with the default provided by the above. For example if your test checks the effect of a method on field
x
, you should only set thex
field in the test, with the rest of the fields taken from the default.
In fact, these are the same rules that apply to code in general.
what are safe patterns of refactoring the tests and allowing new functionality with a new API to grow?
Whenever you find out that an API change makes you change the tests in multiple places, try to figure out how to move the code into a single place. This follows from the above.
Make your tests small. A good test calls maybe 1-3 methods on the subject of the test and does some assertions on the result. These test will only need to change when one of these three methods change.
Make your test code clean. If you haven't already read 'Clean Code' by Robert C. Martin. Apply the rules to your production code AND your test code. This has the tendency to reduce the affected surface area of any refactoring.
Do refactor more often. Instead of (possibly unconsiously) waiting until you must do a large refactoring, do small refactorings a lot.
If you are faced with a huge refactoring, break it down in a couple (or if necessary a couple hundred) tiny refactorings.
In this case, I suggest you get the features locked down, and the iterations short.
Because the iterations are short, features will be grouped together into smaller, isolated groups. It lessens the need to think up of some grand design, which might not be adaptive to the needs of the users. The code for this week, will only work with the code for this week. That lessens the chances of new stuff mucking up the old stuff.
Yeah, it's a problem that is hard to avoid with TDD, since the whole idea behind it is to avoid the over-engineering caused by doing big designs upfront. So with TDD, your design is going to change, and often. The more tests you have, the more effort is required for each refactoring, which effectively discourage it, going against the whole idea behind TDD.
Even though your design will change, your basic requirements will be rather stable, so at the "high level", the way your app works shouldn't change too much. That's why I advise to put all your tests at the "high level" (integration testing, kinda). Low level tests are a liability because you have to change them when you refactor. High level testing is a bit more work, but I think it's worth it in the end.
I wrote an article about that a year ago: http://www.hardcoded.net/articles/high-level-testing.htm
The following might help..
- Follow good coding standards and practices, including TDD and utilising design patterns to produce well structured designs. The application as a whole should then be easier to extend to include new features.
- Good separation of concerns. If your API is well separated from the other functionality (calculations, database access etc) then changing the functionality without changing the API or vice-versa should be easier.
- Use BDD to provide automated tests at a higher level (ie. more like user tests than unit tests). These should help to give you confidence while refactoring even if all your unit tests are breaking due to the refactoring.
- Use a Dependency Injection Container such as Windsor. By abstracting away your class dependencies when those dependencies change it creates much less re-work (particularly if you have many unit tests) than having them coded into your classes.
You shouldn't find yourself "in a corner" with TDD. The eleventh test shouldn't so seriously change the design that the first ten tests need to change. Think hard about why so many tests need to change - look at it in detail, one by one - and see if you can come up with a way to make the change without breaking your existing tests.
If, for instance, you need to add a parameter to a method that they all call:
- you could leave the existing fewer-parameter method in place, delegating to the new method and adding a default parameter;
- you could have all the tests call a utility method (or perhaps a setup method) that calls the old method, so you need to change the method call in only one place;
- you may be able to let your IDE do all the changes with a single command.
TDD and Refactoring work symbiotically; each helps the other. Because you emerge from TDD with comprehensive unit tests, refactoring is safe; because you have the organizational, intellectual, and editing tools to refactor freely, you can keep your tests well synchronized with your design. You say you are a beginner at TDD; perhaps you need to be growing your refactoring skills while you learn TDD.
精彩评论