Updating Tests and Code in Small Steps


At its heart, science is a way of discovering the world by making small, controlled, testable hypotheses, testing them, and seeing what happens.

At its heart, a lot of programming is the same way. (So is running a small business selling a new product or service.)

At its heart, test-driven development is a scientific process. When done well, it makes and verifies assertions about the reality of the software and the needs it meets. You might even call some of these assertions axioms. If every substantive program writes its own rules of physics and reality, our tests exercise and demonstrate these rules just as the well understood experiments of gravity and light and motion demonstrate our understanding of the physical world.

Unlike the nature of reality (unless you're a solipsist, in which case you already know what I'm going to write next, so close this browser window and go outside), we control both sides of the experiment in our software. We write the tests and we write the code. This gives us a disadvantage, in that we're all optimists and rarely expect things to go wrong, but it also gives us an advantage, in that if we let the tests drive the low-level design and implementation, we get very quick feedback on accuracy, utility, and usability.

We also have the tremendous advantage that we can change the world to meet our expectations just as we change our expectations to reflect the world.

Many people claim one of the benefits of a comprehensive test suite is the confidence it gives you that you can change the design of your software without inadvertently changing behavior. It's a safety net. However you want to meet your interface and behavior promises, you do that. If anything changes for the worse, the tests will warn you.

This often works very well.

I like the original idea of refactoring for two reasons. First, it made clear the distinction between changing behavior and changing design. It's like switching out one stable sorting algorithm for another. If they both produce the same results, you can choose which one you prefer based on other concerns, such as performance or memory use or clarity or maintainability. The other refactoring characteristic is that all refactorings are small and reversible. Just as you can extract a method, so you can inline a method. If you haven't changed the public interface, switch back and forth between the two until you find the right design for your software.

(Some days I feel the temptation to become a linguistic prescriptivist, and today because the perfectly good word "refactoring" has come to be a snooty synonym for "rewriting a chunk of code and probably its interface" among people who are in the know.)

If your tests break during a refactoring, either you weren't really refactoring (you were rewriting) or you made an assumption somewhere in your reality model and need to rethink that part of your testing to make it more robust.

While it's important to keep the definition of "refactoring" pure (lest we lose the notions of design change with retained behavior and reversibility), it's also useful to give ourselves permission to change our tests in small ways to force us to change our code in small ways.

I repeat the word "small" for good reason.

My rule is this: as long as all of my tests pass (every one, no exceptions), I can check in anything I like on a branch in my repository. The corollary is that I can't check in anything if tests are failing. No matter how small the change, if my tests all pass, I can check it in. If it's a one-line change and the tests pass, I can check it in.

Giving myself the freedom to change reality and my conception of reality in as many small steps as possible lets me work in the same style as refactoring while changing behavior. I don't steal the name, but I do borrow the idea.

This means that I have to keep giving myself the permission to introduce intermediate steps from where I am to where I want to be. I know the code I check in now won't be the code I merge back in an hour, but that's okay. The code I check in now passes the tests I check in now. With every step of the process I change the universe, but every change in the universe comes with a verification that that's the universe I wanted at that point in time.

It's not quite refactoring. Perhaps it needs a better name. It's an immensely powerful trick: start with a universe, change your experiments slightly, then change the universe to match. It's the tortoise versus the hare: small steps, independently verified, which add up to big changes.

All you have to do is give yourself permission to work in stages so small that you can verify them in a couple of seconds, then use that discipline as a lever with which to move the world.


Well, it may just be your talk of "science" tickling my suppressed biologist tendencies, but I'd call that process "evolution"...

Hopefully the fitness functions have a lot more design behind them.

Or at the very least that the process doesn't resemble a random walk quite so much -- agreed. "Ratcheting", perhaps, gives a better combination of "iteration" and "incremental positive movement".

I often think of the word "ratcheting" when I'm doing this, but I didn't know how well that image would work for other people.

Modern Perl: The Book

cover image for Modern Perl: the book

The best Perl Programmers read Modern Perl: The Book.

sponsored by the How to Make a Smoothie guide



About this Entry

This page contains a single entry by chromatic published on June 6, 2012 6:00 AM.

From Alchemy to Science in Programming was the previous entry in this blog.

The Reluctant Perl Programmer is the next entry in this blog.

Find recent content on the main index or look in the archives to find all content.

Powered by the Perl programming language

what is programming?