ruby-on-railsrspecrspec-railsrspec3

Negating a custom RSpec matcher that contains expectations


I have a custom RSpec matcher that checks that a job is scheduled. It's used like so:

expect { subject }.to schedule_job(TestJob)

schedule_job.rb:

class ScheduleJob
  include RSpec::Mocks::ExampleMethods

  def initialize(job_class)
     @job_class = job_class
  end

  ...

 def matches?(proc)
   job = double
   expect(job_class).to receive(:new).and_return job
   expect(Delayed::Job).to receive(:enqueue).with(job)
   proc.call
   true
 end

This works fine for positive matching. But it does not work for negative matching. e.g:

expect { subject }.not_to schedule_job(TestJob) #does not work

For the above to work, the matches? method needs to return false when the expectations are not met. The problem is that even if it returns false, the expectations have been created regardless and so the test fails incorrectly.

Any ideas on how to make something like this work?


Solution

  • I had to look for it, but I think it's nicely described here in the rspec documentation

    Format (from the docs) for separate logic when using expect.not_to:

    RSpec::Matchers.define :contain do |*expected|
      match do |actual|
        expected.all? { |e| actual.include?(e) }
      end
    
      match_when_negated do |actual|
        expected.none? { |e| actual.include?(e) }
      end
    end
    
    RSpec.describe [1, 2, 3] do
      it { is_expected.to contain(1, 2) }
      it { is_expected.not_to contain(4, 5, 6) }
    
      # deliberate failures
      it { is_expected.to contain(1, 4) }
      it { is_expected.not_to contain(1, 4) }
    end