- Add a feature - Add some behavior, hold existing behavior constant
- Fix a bug - Change some behavior, hold other behavior constant
- Improve design - Change code structure, hold existing behavior constant
- Optimize resource usage - Change code to improve resource usage, hold existing behavior constant
Detecting changes in existing behavior is important!
Legacy code is code without tests. -- Michael Feathers
When we change code, we should have tests in place. To put tests in place, we often have to change code.
- Identify Change Points
- Find Test Points
- Break Dependencies
- Write Tests
- Make Changes and Refactor
- Hyper-aware Editing
- Single Goal Editing
- Preserve Signatures
- Lean on the Compiler
- Pair Programming
A seam is a place where you can alter behavior in your program without editing in that place.
- Characterizes the actual behavior of the code.
- Use white box testing to identify useful input values
- Assert on the current actual results
- An interception point is simply a point in your program where you can detect the effects of a particular change. Make this as close to your change points as you can.
- Automated Refactoring to introduce basic seams and break dependencies
- Cover with Characterization Tests and Regular Unit Tests
- Introduce seams at the change and interception points using less safe refactorings (if needed)
- TDD change
We break dependencies:
- so we can sense when we can't access values our code computes
- to separate when we can't even get a piece of code into a test harness to run.
- verify
- getters and non-private fields
- when
- avoid using real resources
- helps write maintainable tests
Testable & Clear > Testable & Muddy > Untestable & Clear > Untestable & Muddy
When you break dependencies in legacy code, you often have to suspend your sense of aesthetics a bit. Some dependencies break cleanly; others end up looking less than ideal from a design point of view. They are like the incision point in surgery: There might be a scar left in your code after your work, but everything beneath it can get better.
If later you can cover the code around the point where you broke the dependencies, you can heal the scar too.
- New features
- Design Improvements
- Tests
- Inject a dependency instead of leaving it internal to a class
- Inject a dependency instead of leaving it internal to a method
- Introduce a method and TDD that
- Introduce a class and TDD that
- Extract method you want to change into a new class and test that
- Test a subclass of your real class and override methods with dependencies
- Extract tough dependency and override it then test child class
- Move constructor dependency to a method and override it
- Pull the parts of a class you want to test into a new abstract base class then test a child of that
- Make current class abstract and push your dependencies into a child class. Test through a test child class.
- Change existing method to be static (if it can be). You can test without an instance
- Introduce a method that contains an existing method and a call to you new method
- Wrap your hard to test class with a Decorator and TDD the decorator
- Use adapter pattern on tough dependency
- Introduce new class that holds your global which it exposes with a getter
- Introduce a new class that contains related global methods
- Pass the values from an object instead of the object
- Add a setInstance to your existing Singleton (Danger!)
- Refactor the code to understand it better, then throw it away.
- Use only automated refactorings, then check it in. <- Bill's version
- Use automated refactorings to make different code blocks identical
- Extract method or variable (IDE does the rest)
- Do example
- Bulleted Method - indentation is not the most obvious problem
- Snarled Method - indentation makes you dizzy
- Modify state or report state. Getters should be idempotent