Chapter 2: Working with Feedback

There are two basic ways to change a system. Feathers calls them Edit and Pray and Cover and Modify.

Edit and Pray is the industry default: plan the change carefully, study the code, make the edit, then run the system and poke around to convince yourself nothing broke. It looks like “working with care” — and the care is real, especially for invasive changes — but care alone isn’t safety. You wouldn’t pick a surgeon who operated with a butter knife just because he was careful. Effective change needs the right tools and techniques, not just attention.

Cover and Modify works with a safety net. The net is a set of tests draped over the code so bad changes can’t leak out and infect the rest of the system. With good tests around a piece of code, you make a change and find out almost immediately whether the effect was good or bad. You still work with care, but now you have feedback.

What is unit testing?

A unit test exercises an individual component in isolation. What counts as a “unit” varies, but it’s usually the most atomic behavioural unit of the system: a function in procedural code, a class in object-oriented code.

A test harness is the testing code you write to exercise a piece of software, plus whatever is needed to run it. xUnit-style frameworks are the common tooling for building harnesses.

Good unit tests share two qualities: they run fast, and they help localise problems. Feathers’ rule of thumb is blunt — a test that takes a tenth of a second is already slow, and if a test doesn’t run fast, it isn’t a unit test.

A test is not a unit test if it:

  1. Talks to a database
  2. Communicates across a network
  3. Touches the file system
  4. Requires special environment setup (e.g. editing config files) to run

Those tests aren’t bad and are often worth writing — but keep them separate from true unit tests so you always have a fast set to run on every change.

Test coverings

Given the choice, it’s always safer to have tests around the code you change. We’re human; we introduce errors. Covering code with tests before changing it makes us far more likely to catch those mistakes.

Dependency is the central problem. Much of the work in legacy code is breaking dependencies so that change — and testing — becomes possible. This creates a chicken-and-egg bind Feathers calls the legacy code dilemma: to change code safely we want tests in place, but to get tests in place we often have to change the code first.

Two method-level refactorings for breaking dependencies on awkward parameters:

  • Primitivise Parameter — replace a complex parameter (used only to fetch some data) with the already-fetched data itself.
  • Extract Interface — when the above doesn’t fit and the parameter is a concrete type, pull an interface out of it so a fake can be substituted under test.

Do these initial refactorings very conservatively. Breaking dependencies often means suspending your sense of aesthetics for a while — some break cleanly, others leave the design looking worse than ideal. They’re like incision points in surgery: there may be a scar afterward, but everything beneath it can heal. Once you can get tests around the area where you broke the dependency, you can clean up the scar too.

The Legacy Code Change Algorithm

The book’s overall recipe for changing legacy code:

  1. Identify change points.
  2. Find test points.
  3. Break dependencies.
  4. Write tests.
  5. Make changes and refactor.

Source

Working Effectively with Legacy Code, Michael Feathers — Chapter 2, Working with Feedback.