on
How to Test Rake Tasks in RSpec Without Rails
Recently in a Ruby project that does not use Rails, I had to write a Rake task. I wanted to be sure it was tested, but I couldn’t figure out how to load the task in the test environment.
Thanks to RSpec Tests for Rake Tasks I was able to get the majority of the setup done. But that post is for Rails projects!
Want the TL;DR? Skip to the code below and look for the line with Rake::DefaultLoader.new.load
.
Writing a Rake Task
I put the tasks in lib/tasks/
and organize them by namespace. So this task lives in lib/tasks/authors.rake
.
namespace :authors do
desc 'Migrate Author Names'
task :migrate_names do
Authors.find_each do |author|
author.update!(full_name: "#{author.first_name} #{author.last_name")
end
end
end
Loading the Tasks Into Rake
This lets you run rake authors:migrate_names
. Add this line to your Rakefile.
Dir.glob('lib/tasks/*.rake').each { |r| load r }
Loading the Tasks in RSpec
The code below does a few things:
- Creates a module that allows you to name a test
rake authors:create
and will load asubject
namedtask
that returns the rake task. - Loads all the rake tasks into memory using
Rake::DefaultLoader
. Since we’re not callingrake
directly in the tests, we need this line to tell the tests where our tasks live. - Adds metadata to any file in
spec/tasks
so that RSpec knows these aretask
tests and need theTaskFormat
module loaded.
require 'rake'
module TaskFormat
extend ActiveSupport::Concern
included do
let(:task_name) { self.class.top_level_description.sub(/\Arake /, '') }
let(:tasks) { Rake::Task }
# Make the Rake task available as `task` in your examples:
subject(:task) { tasks[task_name] }
end
end
RSpec.configure do |config|
config.before(:suite) do
Dir.glob('lib/tasks/*.rake').each { |r| Rake::DefaultLoader.new.load r }
end
# Tag Rake specs with `:task` metadata or put them in the spec/tasks dir
config.define_derived_metadata(file_path: %r{/spec/tasks/}) do |metadata|
metadata[:type] = :task
end
config.include TaskFormat, type: :task
end
Writing the Tests
Now lets write tests! The tests below use verifying doubles to avoid database transactions, but how you write your tests is up to you!
require_relative '../../support/tasks'
describe 'rake authors:migrate_names', type: :task do
let(:author_cls_double) { class_double(Author).as_stubbed_const(transfer_nested_constants: true) }
let(:author_double) { instance_double(Author, first_name: 'Cassidy', last_name: 'Scheffer') }
let(:expected_name) { 'Cassidy Scheffer'
before do
allow(author_cls_double).to receive(:find_each).and_yield(author_double)
allow(author_double).to receive(:update!).with({ full_name: expected_name })
task.execute
end
it 'updates the correct fields' do
expect(author_double).to have_received(:update!).with({ full_name: expected_name })
end
end