Search code examples
ruby-on-railsrubyrspecrakeresque

Are spies an appropriate approach to see if Resque methods are being fired?


While simple coverage is reporting this as 100% covered I am not satisfied. The spec marked as focus I would like to confirm that all of the Resque methods are being fired. Is a spy or a double the right approach for this?

Spec

describe 'resque tasks' do
  include_context 'rake'
  let(:task_paths) { ['tasks/resque'] }

  before do
    invoke_task.reenable
  end

  # rubocop:disable all
  describe 'resque' do
    context ':setup' do
      let(:task_name) { 'resque:setup' }

      it 'works' do
        invoke_task.invoke
        expect(Resque.logger.level).to eq(1)
      end
    end

    context ':scheduler_setup' do
      let(:task_name) { 'resque:scheduler_setup' }

      it 'works' do
        expect(invoke_task.invoke).to be
      end
    end

    context ':clear', focus: true do
      let(:task_name) { 'resque:clear' }

      it 'works' do
        expect(Resque).to receive(:remove_queue).with('queue:default').and_return(true)
        expect { invoke_task.invoke }.to output(
          "Clearing default...\n"\
          "Clearing delayed...\n"\
          "Clearing stats...\n"\
          "Clearing zombie workers...\n"\
          "Clearing failed jobs...\n"\
          "Clearing resque workers...\n"
        ).to_stdout
      end
    end
  end

  describe 'jobs:work' do
    let(:task_name) { 'jobs:work' }

    it 'works' do
      expect_any_instance_of(Object).to receive(:system).with("bundle exec env rake resque:workers QUEUE='*' COUNT='#{ENV['WEB_WORKERS']}'").and_return(true)
      expect(invoke_task.invoke).to be
    end
  end
  # rubocop:enable all
end

Resque Rake Task

require 'resque'
require 'resque/tasks'
require 'resque/scheduler/tasks'

# http://jademind.com/blog/posts/enable-immediate-log-messages-of-resque-workers/
namespace :resque do
  desc 'Initialize Resque environment'
  task setup: :environment do
    ENV['QUEUE'] ||= '*'
    Resque.logger.level = Logger::INFO
  end

  task scheduler_setup: :environment

  # see http://stackoverflow.com/questions/5880962/how-to-destroy-jobs-enqueued-by-resque-workers - old version
  # see https://github.com/defunkt/resque/issues/49
  # see http://redis.io/commands - new commands
  desc 'Clear pending tasks'
  task clear: :environment do
    queues = Resque.queues
    queues.each do |queue_name|
      puts "Clearing #{queue_name}..."
      Resque.remove_queue("queue:#{queue_name}")
    end

    puts 'Clearing delayed...'
    Resque.redis.keys('delayed:*').each do |key|
      Resque.redis.del key.to_s
    end
    Resque.redis.del 'delayed_queue_schedule'
    Resque.reset_delayed_queue

    puts 'Clearing stats...'
    Resque.redis.set 'stat:failed', 0
    Resque.redis.set 'stat:processed', 0

    puts 'Clearing zombie workers...'
    Resque.workers.each(&:prune_dead_workers)

    puts 'Clearing failed jobs...'
    cleaner = Resque::Plugins::ResqueCleaner.new
    cleaner.clear

    puts 'Clearing resque workers...'
    Resque.workers.each(&:unregister_worker)
  end
end

desc 'Alias for resque:work'
# http://stackoverflow.com/questions/10424087/resque-multiple-workers-in-development-mode
task 'jobs:work' do
  system("bundle exec env rake resque:workers QUEUE='*' COUNT='#{ENV['WEB_WORKERS']}'")
end

Shared Context

shared_context 'rake' do
  let(:invoke_task) { Rake.application[task_name] }
  let(:highline) { instance_double(HighLine) }

  before do
    task_paths.each do |task_path|
      Rake.application.rake_require(task_path)
    end
    Rake::Task.define_task(:environment)
  end

  before do
    allow(HighLine).to receive(:new).and_return(highline)
  end
end

Solution

  • The spec marked as focus I would like to confirm that all of the Resque methods are being fired. Is a spy or a double the right approach for this?

    Yes. A Spy in this test would only be testing that it received those methods calls, since it is acting as a double stand-in for those tests; meaning you are not testing the behaviour of task in this test, you are testing that the task has an object like Resque receiving those method calls.

    Spies

    Message expectations put an example's expectation at the start, before you've invoked the code-under-test. Many developers prefer using an act-arrange-assert (or given-when-then) pattern for structuring tests. Spies are an alternate type of test double that support this pattern by allowing you to expect that a message has been received after the fact, using have_received.

    -- Spies - Basics - RSpec Mocks - RSpec - Relish

    An example of what this might look like for your it 'works' test

    it 'works' do
      expect(Resque).to receive(:remove_queue).with('queue:default').and_return(true)
      expect { invoke_task.invoke }.to output(
        "Clearing default...\n"\
        "Clearing delayed...\n"\
        "Clearing stats...\n"\
        "Clearing zombie workers...\n"\
        "Clearing failed jobs...\n"\
        "Clearing resque workers...\n"
      ).to_stdout
    end
    

    Is as follows

    RSpec.describe "have_received" do
      it 'works' do
        Rake::Task.define_task(:environment)
        invoke_task = Rake.application['resque:clear']
    
        redis_double = double("redis")
        allow(redis_double).to receive(:keys).with('delayed:*').and_return([])
        allow(redis_double).to receive(:del).with('delayed_queue_schedule').and_return(true)
        allow(redis_double).to receive(:set).with('stat:failed', 0).and_return(true)
        allow(redis_double).to receive(:set).with('stat:processed', 0).and_return(true)
    
        allow(Resque).to receive(:queues).and_return([])
        allow(Resque).to receive(:redis).and_return(redis_double)
        # allow(Resque).to receive(:remove_queue).with('queue:default') #.and_return(true)
        allow(Resque).to receive(:reset_delayed_queue) #.and_return(true)
        allow(Resque).to receive(:workers).and_return([])
    
        cleaner_double = double("cleaner")
        allow(Resque::Plugins::ResqueCleaner).to receive(:new).and_return(cleaner_double)
        allow(cleaner_double).to receive(:clear).and_return(true)
    
        expect { invoke_task.invoke }.to output(
          # "Clearing default...\n"\
          "Clearing delayed...\n"\
          "Clearing stats...\n"\
          "Clearing zombie workers...\n"\
          "Clearing failed jobs...\n"\
          "Clearing resque workers...\n"
        ).to_stdout
    
        expect(redis_double).to have_received(:keys)
        expect(redis_double).to have_received(:del)
        expect(redis_double).to have_received(:set).with('stat:failed', 0)
        expect(redis_double).to have_received(:set).with('stat:processed', 0)
    
        expect(Resque).to have_received(:queues)
        expect(Resque).to have_received(:redis).at_least(4).times
        # expect(Resque).to have_received(:remove_queue).with('queue:default')
        expect(Resque).to have_received(:reset_delayed_queue)
        expect(Resque).to have_received(:workers).twice
    
        expect(Resque::Plugins::ResqueCleaner).to have_received(:new)
        expect(cleaner_double).to have_received(:clear)
      end
    end
    

    Notes:

    • The allow(Resque).to receive(:remove_queue).with('queue:default') is commented out since allow(redis_double).to receive(:keys).with('delayed:*').and_return([]) returns an empty array in my example code, meaning that queues.each never iterates once, so Resque.remove_queue("queue:#{queue_name}") is never called and "Clearing default...\n"\ is not return for the expected output

    • Also, there is a lot happening in this one task, and might be worthwhile breaking it down into smaller tasks.

    This effectively stubs each of the expected method calls on the Resque object and then accesses after task has been invoked that the doubles receive those expected method calls. It does not test the outcomes of those tasks, only that method calls occurred and confirms those

    methods are being fired.

    References: