Test Commit Revert in Rails using Guard

Test Commit Revert in Rails using Guard

What is TCR

TCR stands for “Test && Commit || Revert“, a programming workflow coined by Kent Beck which looks like this in practice:

  • Changes are made
  • Tests are run
    • If tests pass, they are committed to the git history.
    • If tests fail the offending changes are reverted.

This workflow is like Test Driven Development (also referred to as TDD) on survival mode; assuming fast feedback loops via autorunning tests, changes are reverted out from under you as the test fails. In my experience this forces you to think differently and very intentionally about the changes you’re making. It’s painful, but a good mental exercise if I ever met one.

For more on TCR, Kent Beck has videos exemplifying this process rather well. The remaining topic of this post is specific to how I set this workflow up as a Ruby on Rails developer.

How to use TCR

There are many ways to approach setting up a TCR workflow, and it is most succinctly described in the initial definition Kent provided with a simple shell script recipe:

Detect a file change.
Execute your test command.
&& commit # this chains the commands together and executes the second command (commit) if and only if the prior command (test in our case) exits without error.
|| revert # || is the OR operator. It executes the command on the right only if the command on the left returned an error.

And the beauty of it is that it does work! I applied this template to some small Ruby personal projects with the flavour of bash magic described above. It’s great! This is roughly what that little bash script looked like:

fswatch -1 . -e ".*" -i "\\.rb$" | xargs -0 -n1 -I{} bin/test-changes && git commit -a -m "working" && ./tcr.sh || git reset --hard && ./tcr.sh

As life happens though, I wanted to take this workflow into my daily work to really get the most out of it. A key aspect of TCR and TDD is the need for a fast feedback loop; I work on years of Ruby on Rails code and with any monolith of a certain age, no matter how majestic, the feedback loop on a rails test run is…tens of minutes. It became clear that I needed to be a little more strategic when I was on Rails.

En Guard

Guard is a gem I have some prior experience with; it can be configured so that when a change is made to a file (for example: models/user.rb) it will autorun the associated test file (in this example: user_test.rb). As you can imagine, this significantly cuts down on the time until seeing either red or green tests which is super helpful for TDD and a necessary first part of TCR.

But then comes the tricky bit; how to build in behaviour around the outcome of whether those tests pass or fail?

This is where things get fun hacky.

Since we use the minitest framework, my work guard setup includes the guard-minitest plugin. Guard supports a great range of plugins; this was another reason it appealed to me compared to solving this problem completely from scratch. I wanted to get something pulled together relatively quickly so I could try this workflow out after all.

Guard also provides some callback functionality that can be used to set up certain behaviours in your Guardfile (the configuration file used to setup guard). On a successful test run, the :run_on_modifications_end event is fired. Great! Super straightforward, I just needed to add the following to my Guardfile:

callback(:run_on_modifications_end) { git commit -a -m "working" }

This covers the Test && Commit portion of our TCR workflow, but what about revert ?

This is where things get super hacky.

What I landed on, you see, is monkey patching the guard-minitest plugin directly in the guardfile so that it throws a specific exception, then in handling that exception it fires off a hook called throw_on_failed and then throws what guard is expecting from the plugin. Now I can implement the revert by adding the following line to my guardfile:

callback(:throw_on_failed) { git checkout . }

With the specific file configurations removed, this is what my offending guardfile looks like:

Now, consider this a mea culpa, as I know there are many ways this could go wrong. The guard documentation was also kind enough to warn me as such. This is, for better or for worse, my first attempt at building out a way to TCR in Rails. I’m excited to be able to follow up as I spend more time with my monstrous guardfile and this workflow so I can let you know how it goes.

Thank you for reading!

References & Inspiration

Kent Beck’s Rubyconf 2020 Keynote (stay for the banjo playing!)

Two key pieces of guard documentation, one on creating an inline guard and the other on hooks and callbacks. All of the docs were very helpful though!


Success! Your account is fully activated, you now have access to all content.
Success! Your billing info is updated.
Billing info update failed.