Here's what I learned from five days of test && commit || revert in Ruby.
Written by Jared Norman
I completed the first five days of this year’s Advent of Code in Ruby using test && commit || revert. The idea behind TCR is pretty simple: every time you run your tests and they pass your changes will automatically be committed. If your tests fail, then instead your uncommitted changes will be thrown away.
Given the basic approach behind Extreme Programming, I find it unsurprising that the first hit on Google for “test commit revert” is an article by Kent Beck; he continues to find clever ways to take programming techniques to extremes.
I’m here to share what I learned in playing with this workflow for a few days.
I did not “properly” test drive all of my code. The Advent of Code puzzles are not an arena where I value extreme rigour. I wanted to complete the puzzles in under an hour, so I took shortcuts. That said, I pretty much always followed the basic flow of writing a test before writing the code to make it pass. I wanted to at least be able to say that for some liberal application of the term that I was “doing TCR”.
No where are my shortcuts more evident than in the validations from day four. I had to write a whole bunch of tedious string validations and only tested them against a valid input and the string “foo” despite them all requiring somewhat nuanced validation.
I wanted to feel out TCR and see what I could learn from and bring back to my regular software development practices and in that I think my approach succeeded.
My TCR harness was a single script that I could invoke that would conditionally do a couple of different things. Running day 3, part 1 of this year’s Advent of Code looked like this:
$ bundle exec aoc.rb 2020 3 1
If I had not yet run script for this day, the script would generate a scaffold for the two parts of the puzzle with an empty RSpec test suite and automatically download the puzzle input for the day.
Running the script subsequent times would run the tests. If the tests passed,
any changes would be committed and the solution for the day would be output. If
the tests failed, the script would run
git reset --hard to throw my changes
The script lent well to TCR and the overall experience. Using it was extremely low friction; a single key binding in Vim would save my changes and run the script. I didn’t need to think about managing git, downloading puzzle inputs, or running my tests and solutions separately.
At every single step, TCR pushes you towards smaller steps. Finding those smaller steps can be tricky. After completing one puzzle, John Hawthorn provided a suggestion for how I could have broken a troublesome step in a way I would never have thought of. This got me thinking about something that I know, but don’t feel like I’ve properly internalized.
It’s okay to write tests and code that you do not intend to keep. It’s even okay to write tests and code that you know to not be the code you need so long as it keeps you moving towards your goal.
My biggest takeway is that I’ve fallen into a habit of aiming to write the final code and tests on the first try. There are often clever ways you can incrementally augment the behaviour of your system (guided by tests) that may cause you to write code that will get refactored away at a later step. This doesn’t make the step any less valid.
How often do you knowingly write code that isn’t going to last more than a couple minutes when working to make a test pass? If you’re fond of “sliming”, you’ve probably already internalized this aspect of TDD. Sadly, many of the projects I work on have test suites that are fairly slow to boot, so I find myself skipping those steps and I think my practice has suffered for it.
Most of the complaints about the idea of TCR are covered in the article I linked at the top. I used “pending” tests (which are expected to fail) so that I could commit failing tests and not lose them. My script would crash when loading if there was a syntax error, so I didn’t lose work due to typos.
My biggest complaint was that on multiple occasions I wrote code that I was 100% sure would work, had it fail, and then rewrote it only to find it worked the second time.
I haven’t gone back and watched the video to find the mistake I made the first time, but having your code thrown away because you didn’t write the code you thought you wrote is frustrating and disorienting. Even if you respond by breaking the problem down further, you still end up unsure of what the mistake you made was unless you happen to have recorded the whole thing. (Lucky me.)
If you’re looking for a way to expand how you think about and approach TDD, I highly recommend taking TCR for a spin. I doubt you’ll decide you want to write all code this way, but with some effort I’m sure you’ll find some clever ways to break down changes into smaller steps, leading to different refactoring options that you might have missed.
If you’d like to see my streams where I tried out TCR, I put this this playlist together with all five days.
Published December 05, 2020