Tim Scott
Tim Scott

Reputation: 15205

Custom RSpec Matcher That Takes A Block

How can a create the following RSpec matcher?

foo.bars.should incude_at_least_one {|bar| bar.id == 42 }

Let me know if I'm reinventing the wheel, but I'm also curious to know how to create a custom matcher that takes a block. Some of the built in matchers do it, so it's possible. I tried this:

RSpec::Matchers.define :incude_at_least_one do |expected|
  match do |actual|
    actual.each do |item|
      return true if yield(item)
    end
    false
  end
end

I aslo tried passing &block at both leves. I'm missing something simple.

Upvotes: 5

Views: 1043

Answers (4)

Taras
Taras

Reputation: 822

For anyone who comes here for the answer, you should use block_arg. Here is the snippet from the RSpec documentation:

require 'rspec/expectations'

RSpec::Matchers.define :be_lazily_equal_to do
  match do |obj|
    obj == block_arg.call
  end

  description { "be lazily equal to #{block_arg.call}" }
end

RSpec.describe 10 do
  it { is_expected.to be_lazily_equal_to { 10 } }
end

Upvotes: 0

Tim Scott
Tim Scott

Reputation: 15205

I started with the code from Neil Slater, and got it to work:

class IncludeAtLeastOne
  def initialize(&block)
    @block = block
  end

  def matches?(actual)
    @actual = actual
    @actual.any? {|item| @block.call(item) }
  end

  def failure_message_for_should
    "expected #{@actual.inspect} to include at least one matching item, but it did not"
  end

  def failure_message_for_should_not
    "expected #{@actual.inspect} not to include at least one, but it did"
  end
end

def include_at_least_one(&block)
  IncludeAtLeastOne.new &block
end

Upvotes: 1

Neil Slater
Neil Slater

Reputation: 27207

The RSpec DSL won't do it, but you could do something like this:

class IncludeAtLeastOne
  def matches?(target)
    @target = target
    @target.any? do |item|
      yield( item )
    end
  end

  def failure_message_for_should
    "expected #{@target.inspect} to include at least one thing"
  end

  def failure_message_for_should_not
    "expected #{@target.inspect} not to include at least one"
  end
end

def include_at_least_one
  IncludeAtLeastOne.new
end

describe "foos" do
  it "should contain something interesting" do
    [1,2,3].should include_at_least_one { |x| x == 1 }
  end
end

Upvotes: 0

Michael Papile
Michael Papile

Reputation: 6856

There has been discussion about adding such a matcher to rspec. I am not sure about your block question but you could represent this test in the not as elegant looking:

foo.bars.any?{|bar| bar.id == 42}.should be_true

Probably easier than making a custom matcher and should be readable if your test is something like it "should include at least one foo matching the id"

Upvotes: 0

Related Questions