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 a subject named task that returns the rake task.
  • 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.
  • 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
    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

    51

    This website collects cookies to deliver better user experience

    How to Test Rake Tasks in RSpec Without Rails