test && commit || revert
(TCR) is a coding workflow proposed by extreme programming guru Kent Beck. I just tried it out. Here’s what I learned.
You have to write tests that pass, otherwise they get deleted. So you end up aiming for silly things, like tests that test nothing or test stubs. After that, it seems to work well enough for something like a Fibonacci function.
The idea is not that this workflow is better, only that it has something to teach us:
When your change being wrong means it being deleted, you become more cautious. Does that sound harsh?
Curious if it’s making a difference in your commit sizes? Here’s a script to compute Git diff size histograms. Choose between logarithmic or constant bin size.
#!/bin/sh -e
# This command prints out a histogram of diff size.
git log --pretty=format:'%p %h' |
while read pair; do
git diff --shortstat $pair
done |
mawk '
function constant_bin(diff_size, bin_size) {
return bin_size * int(diff_size / bin_size)
}
function log_bin(diff_size, base) {
return exp(log(base) * int(log(diff_size) / log(base)))
}
{
diff_size = $4 + $6
freq[log_bin(diff_size, 2)]++
#freq[constant_bin(diff_size, 2)]++
}
END {
for (diff_size in freq) {
print diff_size, freq[diff_size]
}
}' | sort -nr
The basic command is
./test && git commit -a || git reset --hard
I’ve shot myself in the foot a few times with this form. I deleted it when I got tired of losing hours of good work. See if you can figure out what the risks are.
./test
command is missing, your changes are deletedgit commit -a
is bad: all changes to tracked files are added, but you can’t review the diff before committingThis is a more careful approach:
#!/bin/sh
if [ ! -x ./test ]; then
printf 'no ./test to run\n'
exit 1
fi
if ./test; then
git commit -v -a
exit 0
fi
git reset --hard
Here, we simply exit with a nonzero status if the test command doesn’t exist. If it does exist, we run it. If it succeeds, we commit all changes, but show the diff in the message template, so you know exactly what has changed. If you abort the commit, the script simply returns. Only if the test fails are the changes deleted.
One ambiguity in TCR is whether it is meant to apply to tests as well as to source code.
In TDD, tests are written first, and their failure unreflectively determines what the code should do. In TCR, failing tests cause changes to revert, preventing you from committing them!
TCR does not prevent you from writing untested code. For that, you’d have to compute test coverage. But being a slave to coverage metrics does not necessarily produce better code, as it can encourage lots of indirection.
It presents a novel challenge. I experienced a flow state while using it.
But it’s not fun enough to continue using it after a month. There are definitely cases where it’s frustrating, like when you can’t figure out how to refactor something without tests breaking somewhere. But it’s useful for encouraging small, strategic changes.
There are a few occasions where I’ve actually used TCR on production codebases. You can spot these by big spikes in the number of commits.
Discuss this page by emailing my public inbox. Please note the etiquette guidelines.
© 2024 Karl Schultheisz