TDD, TCR, and Frequent Commits

We’ll look at Test-Driven Development (TDD) and Test-Commit-Revert (TCR) in terms of how they work with frequent commits to source control. 

Test-Driven Development (TDD)

Test-Driven Development is often described with this cycle: Red-Green-Refactor (or Red-Green-Gold, as I learned from Alex Freire [who doesn’t claim to have originated it]). A picture like this is common:

TDD: Red-Green-Refactor

Mike Hill describes adding integration to this loop (also described in Industrial Logic’s eLearning):

TDD: Red-Green-Refactor-Integrate

Jay Bazuzi describes tagging commits frequently to indicate whether they are refactorings, new code, etc. (I’ve heard Arlo Belshee describe a similar approach.)

TDD With Commits

Let’s take a different view of TDD: Let’s look in terms of the evolving software if we commit every time the tests all run green.

TDD with Commits

We start with version n, and all tests green. One thing we can do is the red-green side: write a test that we expect to fail, verify that it does fail, then write production code to make it (and all existing tests) pass. Since the tests are “green”, it’s safe to check in, and we end with a system that is slightly more functional, and still passing its tests.

Or, we could refactor: start green, do a proper refactoring — that changes the design but leaves the behavior the same. All tests will still be green, and it’s safe to check in. 

(We can imagine other kinds of moves, such as deleting a test or writing a test we expect to run green, confirming that the system already behaves a certain way, but we’ll just stick with TDD tests here.)

With this approach, a red-green-refactor cycle results in two commits, one for the red-green, and a second for the refactoring. 

Models are simplifications. The above model describes a TDD process where everything goes as expected. But one of the things I’ve learned about testing state machines applies to this model: you learn a lot when you’re explicit about every possible transition. 

TDD When Things Go Wrong

What happens when everything doesn’t go as planned?

TDD with Commits and Reverts

Dashed lines show the flow when things happen unexpectedly. Let’s start with “Write a test (expected to fail)”. If that test turns out to pass, it means the system unexpectedly already has the behavior we had planned to add. This usually means one of two things: we were ignorant of the behavior of the system, or else our test is wrong and isn’t properly testing the intended new behavior. 

Another thing that might go wrong is that we write production code we expect to make tests pass, but some test fails. It might be that our new production code is wrong. If so, we can revert just those changes (perhaps via “Undo”). 

Alternatively, we might realize that it’s not the new production code, but rather the test itself that is not right, Then we can return to the original version (before we wrote that test) and try to write a better test (or refactor instead). 

The final bad situation happens when a supposed refactoring actually changes behavior (detected by a failing test). Then we revert the refactoring and try again.

This may seem heavy on reverting to you, and many times people won’t be strict about this: they’ll realize what they did wrong and push forward, rather than back up and try again. (I’ve certainly done this. And that may be your style too.) But each time it happens, the process is telling you, “You don’t know what you’re doing as much as you think.” Listen to that little bit of feedback. 

Test && Commit || Revert (TCR)

TCR stands for “Test && Commit || Revert”. It’s not TDD per se, but a related approach.

TCR was invented at a workshop Kent Beck was doing, based on an observation by Oddmund Strømme that the normal TDD workflow isn’t symmetric. You can see that in the pictures above: sometimes you want tests passing, sometimes failing; sometimes you want to commit, sometimes you don’t. 

“Test && commit || revert” means: write some amount of tests and/or production code, then run the tests. If the tests succeed, commit the code. If the tests fail, revert the code. This certainly encourages small steps: you don’t want to revert a lot of code because of one tiny mistake. 

Here’s how TCR looks:

TCR: Test && Commit || Revert

Isn’t that beautiful symmetry? You make a move; revert if it fails, and commit if it succeeds.

TCR is a simpler workflow, and definitely challenges your ability to take small, safe steps. 

Conclusion

These programming workflows are similar, in that they deal with tests, new code, and refactoring. They act differently with respect to high-frequency commits. The TDD process gets a bit complicated, but TCR has simplicity built in.