I’m extremely late to the Exercism party, but I’ve been having lots of fun working my way through its Ruby track (see my Exercism profile). The only thing I have missed while working on the exercises is the automated workflows that I would normally have: specifically, having tests and Rubocop run automatically after any file has changed.
When I work on any Ruby or Rails project, I immediately reach for Guard to help me out with running these kinds of processes, and this time will be no different. However, the structure of a (completed) Ruby Exercism exercise (located by default under ~/exercism/ruby/
) does not look like a typical Ruby project:
my_exercise
├── README.md
├── my_exercise.rb
└── my_exercise_test.rb
It is a single directory with both the implementation and test file in it, which is not the kind of project setup that Guard expects will be used with Ruby. So, Guard will need some extra help on the configuration side of things to figure out how to deal with this. But first, let’s install some gems to get started.
Install Gems
As well as Guard itself, we’re going to want to install the following other gems:
- Guard::Minitest: Tests in Exercism are written using Minitest, so this gem will make sure Guard launches the tests with the Minitest framework.
- guard-rubocop: Runs Rubocop when files are modified.
Since there is no Bundler or Gemfile
in sight, we will be installing these gems globally:
If you use the asdf version manager, add these gems to your default gems file so that you don’t need to worry about manually installing them globally again if you update your Ruby version.
gem install guard guard-minitest guard-rubocop
Generate Guardfile
After installing the gems, change into your exercism/ruby
directory (wherever it is installed on your system), and generate the Guardfile
:
guard init
If you find this command does not work, depending on your Ruby version manager, you may need to perform a reshim of Ruby executables.
Typically, you will have one Guardfile
per Ruby project, but rather than have one per Exercism exercise, which will get old very fast, the plan is to have a single Guardfile
that any Ruby exercise can use, and that’s why we generated it in the top level Ruby directory.
The generated Guardfile
will look something like this:
guard :minitest do
# with Minitest::Unit
watch(%r{^test/(.*)\/?test_(.*)\.rb$})
watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}test_#{m[2]}.rb" }
watch(%r{^test/test_helper\.rb$}) { 'test' }
# with Minitest::Spec
# watch(%r{^spec/(.*)_spec\.rb$})
# ...
# Rails 4
# watch(%r{^app/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" }
# ...
# Rails < 4
# ...
end
guard :rubocop do
watch(%r{.+\.rb$})
watch(%r{(?:.+/)?\.rubocop(?:_todo)?\.yml$}) { |m| File.dirname(m[0]) }
end
Guard Minitest configuration
First, delete all the non-Minitest::Unit
configuration to get that out of the way, and let’s take a closer look at exactly what the remaining configuration does:
guard :minitest do
# When a test file (defined as a Ruby file that starts with `test_`)
# located under the `test/` directory is modified, run that test.
watch(%r{^test/(.*)\/?test_(.*)\.rb$})
# When a Ruby file located under the `lib/` directory is modified,
# run the test file located under the `test/` directory for that Ruby file.
watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}test_#{m[2]}.rb" }
# When the `test/test_helper.rb` file is modified, run the entire test suite.
watch(%r{^test/test_helper\.rb$}) { 'test' }
end
Unfortunately, it looks like we cannot use any of Guard’s default configuration here as-is because:
- In Exercism, everything is in the same directory, so there are no
lib/
ortest/
directories to go looking in. - The naming for Exercism test files is
*_test.rb
, nottest_*.rb
. - There is no
test_helper.rb
file.
So, we’re going to have to re-write the configuration from scratch, but before we do that, let’s determine what we actually want Guard to do for us within an Exercism exercise directory. For me at least, what I would want is:
- If I modify the test file under the exercise root directory, run it again
- If I modify the implementation file under the exercise root directory, run its test file, which is also located under the exercise root directory
To do that, I came up with the following:
guard :minitest, test_folders: ["."] do
# Re-test test files when they're modified.
watch(%r{\A.+_test\.rb\z}) { |m| "./#{m[1]}" }
# Run the test file of the implementation (non-test) file that was modified.
watch(%r{\A(.+)(?<!_test)\.rb\z}) { |m| "./#{m[1]}_test.rb" }
end
Let’s examine some of the reasons behind these lines:
- The
test_folders
flag was added to specifically tell Guard::Minitest that test files are located in the current directory ("."
) because by default it will look insidetest
orspec
directories. - The string values in the blocks for both
watch
functions need to resemble a path, otherwise no processes would run. For example,watch(%r{\A.+_test\.rb\z}) { |m| "#{m[1]}" }
would not work: the block value needs to be"./#{m[1]}"
(figuring this out was a painful gotcha). - Since both implementation and test file are in the same directory, the last statement uses a negative lookbehind assertion (
(?<!_test)
) to make sure that when./bob.rb
is modified,./bob_test.rb
is run, but when./bob_test.rb
is modified, Guard does not attempt to run a non-existent./bob_test_test.rb
file.
Guard Rubocop configuration
Much like it is easier to have one Guardfile
that can be used for all Exercism exercises, the same is true for your Rubocop configuration file (.rubocop.yml
). If you have a specific .rubocop.yml
file that you want to use just for Exercism, then place it under your exercism/ruby
directory, and it will get found when you run the rubocop
command.
For the guard-rubocop configuration, we will need to re-write the first rule slightly (watch(%r{.+\.rb$})
) because it currently will run Rubocop over all Ruby files, including the Exercism test file, which is not written by us (and therefore we are not responsible for whether it is written to Rubocop’s standards). So, we’ll use a similar negative lookbehind assertion to fix that problem:
guard :rubocop do
# Only run Rubocop over implementation files
# as test files are not written by students.
watch(%r{\A(.+)(?<!_test)\.rb\z})
watch(%r{(?:.+/)?\.rubocop\.yml\z}) { |m| File.dirname(m[0]) }
end
Putting it all together
My final ~/exercism/ruby/Guardfile
, with some extra bits of configuration, looks like the following:
# frozen_string_literal: true
group :red_green_refactor, halt_on_fail: true do
guard :minitest, all_on_start: false, test_folders: ["."] do
# Re-test test files when they're edited.
watch(%r{\A.+_test\.rb\z}) { |m| "./#{m[1]}" }
# Run the test file of the file that was edited.
watch(%r{\A(.+)(?<!_test)\.rb\z}) { |m| "./#{m[1]}_test.rb" }
end
guard :rubocop, all_on_start: false, cli: ["--display-cop-names"] do
# Only run Rubocop over implementation files
# as test files are not written by me.
watch(%r{\A(.+)(?<!_test)\.rb\z})
watch(%r{(?:.+/)?\.rubocop\.yml\z}) { |m| File.dirname(m[0]) }
end
end
The optional red_green_refactor
group idea is lifted directly from the guard-rubocop README
file, and makes perfect sense to me: get your tests passing first, and only then worry about whether your code looks nice.
This configuration might change over time, so you can always get the latest version from my Exercism Github repo.
Running Guard
Now that the Guardfile
is set up with Minitest and Rubocop, you will need to make sure to tell Guard where to find this configuration when you run it:
guard --guardfile ~/exercism/ruby/Guardfile
And that should be it! You should now have a pair of friendly robots looking over your shoulder while you’re solving the exercises, helping you to submit the best solution that you can.
Leave a comment