DEV Community

Cassidy Scheffer (she/her)
Cassidy Scheffer (she/her)

Posted 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
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

Loading the Tasks in RSpec

The code below does a few things:

  1. Creates a module that allows you to name a test rake authors:create and will load a subject named task that returns the rake task.
  2. Loads all the rake tasks into memory using Rake::DefaultLoader. Since we're not calling rake directly in the tests, we need this line to tell the tests where our tasks live.
  3. Adds metadata to any file in spec/tasks so that RSpec knows these are task tests and need the TaskFormat 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Discussion (0)