yield matchers

There are four related matchers that allow you to specify whether or not a method yields, how many times it yields, whether or not it yields with arguments, and what those arguments are.

* `yield_control` matches if the method-under-test yields, regardless of whether or not
  arguments are yielded.
* `yield_with_args` matches if the method-under-test yields with arguments. If arguments
  are provided to this matcher, it will only pass if the actual yielded arguments match the expected ones using `===` or `==`.
* `yield_with_no_args` matches if the method-under-test yields with no arguments.
* `yield_successive_args` is designed for iterators, and will match if the method-under-test
   yields the same number of times as arguments passed to this matcher, and all actual yielded arguments match the expected ones using `===` or `==`.

Note: your expect block must accept an argument that is then passed on to the method-under-test as a block. This acts as a “probe” that allows the matcher to detect whether or not your method yields, and, if so, how many times and what the yielded arguments are.

Background

Given a file named “my_class.rb” with:

class MyClass
  def self.yield_once_with(*args)
    yield *args
  end

  def self.yield_twice_with(*args)
    2.times { yield *args }
  end

  def self.raw_yield
    yield
  end

  def self.dont_yield
  end
end

The yield_control matcher

Given a file named “yieldcontrolspec.rb” with:

require './my_class'

RSpec.describe "yield_control matcher" do
  specify { expect { |b| MyClass.yield_once_with(1, &b) }.to yield_control }
  specify { expect { |b| MyClass.dont_yield(&b) }.not_to yield_control }
  specify { expect { |b| MyClass.yield_twice_with(1, &b) }.to yield_control.twice }
  specify { expect { |b| MyClass.yield_twice_with(1, &b) }.to yield_control.exactly(2).times }
  specify { expect { |b| MyClass.yield_twice_with(1, &b) }.to yield_control.at_least(1) }
  specify { expect { |b| MyClass.yield_twice_with(1, &b) }.to yield_control.at_most(3).times }

  # deliberate failures
  specify { expect { |b| MyClass.yield_once_with(1, &b) }.not_to yield_control }
  specify { expect { |b| MyClass.dont_yield(&b) }.to yield_control }
  specify { expect { |b| MyClass.yield_once_with(1, &b) }.to yield_control.at_least(2).times }
  specify { expect { |b| MyClass.yield_twice_with(1, &b) }.not_to yield_control.twice }
  specify { expect { |b| MyClass.yield_twice_with(1, &b) }.not_to yield_control.at_least(2).times }
  specify { expect { |b| MyClass.yield_twice_with(1, &b) }.not_to yield_control.at_least(1) }
  specify { expect { |b| MyClass.yield_twice_with(1, &b) }.not_to yield_control.at_most(3).times }
end

When I run rspec yield_control_spec.rb

Then the output should contain all of these:

13 examples, 7 failures
expected given block to yield control
expected given block not to yield control
expected given block not to yield control at least twice
expected given block not to yield control at most 3 times

The yield_with_args matcher

Given a file named “yieldwithargs_spec.rb” with:

require './my_class'

RSpec.describe "yield_with_args matcher" do
  specify { expect { |b| MyClass.yield_once_with("foo", &b) }.to yield_with_args }
  specify { expect { |b| MyClass.yield_once_with("foo", &b) }.to yield_with_args("foo") }
  specify { expect { |b| MyClass.yield_once_with("foo", &b) }.to yield_with_args(String) }
  specify { expect { |b| MyClass.yield_once_with("foo", &b) }.to yield_with_args(/oo/) }

  specify { expect { |b| MyClass.yield_once_with("foo", "bar", &b) }.to yield_with_args("foo", "bar") }
  specify { expect { |b| MyClass.yield_once_with("foo", "bar", &b) }.to yield_with_args(String, String) }
  specify { expect { |b| MyClass.yield_once_with("foo", "bar", &b) }.to yield_with_args(/fo/, /ar/) }

  specify { expect { |b| MyClass.yield_once_with("foo", "bar", &b) }.not_to yield_with_args(17, "baz") }

  # deliberate failures
  specify { expect { |b| MyClass.yield_once_with("foo", &b) }.not_to yield_with_args }
  specify { expect { |b| MyClass.yield_once_with("foo", &b) }.not_to yield_with_args("foo") }
  specify { expect { |b| MyClass.yield_once_with("foo", &b) }.not_to yield_with_args(String) }
  specify { expect { |b| MyClass.yield_once_with("foo", &b) }.not_to yield_with_args(/oo/) }
  specify { expect { |b| MyClass.yield_once_with("foo", "bar", &b) }.not_to yield_with_args("foo", "bar") }
  specify { expect { |b| MyClass.yield_once_with("foo", "bar", &b) }.to yield_with_args(17, "baz") }
end

When I run rspec yield_with_args_spec.rb

Then the output should contain all of these:

14 examples, 6 failures
expected given block not to yield with arguments, but did
expected given block not to yield with arguments, but yielded with expected arguments
expected given block to yield with arguments, but yielded with unexpected arguments

The yield_with_no_args matcher

Given a file named “yieldwithnoargsspec.rb” with:

require './my_class'

RSpec.describe "yield_with_no_args matcher" do
  specify { expect { |b| MyClass.raw_yield(&b) }.to yield_with_no_args }
  specify { expect { |b| MyClass.dont_yield(&b) }.not_to yield_with_no_args }
  specify { expect { |b| MyClass.yield_once_with("a", &b) }.not_to yield_with_no_args }

  # deliberate failures
  specify { expect { |b| MyClass.raw_yield(&b) }.not_to yield_with_no_args }
  specify { expect { |b| MyClass.dont_yield(&b) }.to yield_with_no_args }
  specify { expect { |b| MyClass.yield_once_with("a", &b) }.to yield_with_no_args }
end

When I run rspec yield_with_no_args_spec.rb

Then the output should contain all of these:

6 examples, 3 failures
expected given block not to yield with no arguments, but did
expected given block to yield with no arguments, but did not yield
expected given block to yield with no arguments, but yielded with arguments: [“a”]

The yield_successive_args matcher

Given a file named “yieldsuccessiveargs_spec.rb” with:

def array
  [1, 2, 3]
end

def array_of_tuples
  [[:a, :b], [:c, :d]]
end

RSpec.describe "yield_successive_args matcher" do
  specify { expect { |b| array.each(&b) }.to yield_successive_args(1, 2, 3) }
  specify { expect { |b| array_of_tuples.each(&b) }.to yield_successive_args([:a, :b], [:c, :d]) }
  specify { expect { |b| array.each(&b) }.to yield_successive_args(Integer, Integer, Integer) }
  specify { expect { |b| array.each(&b) }.not_to yield_successive_args(1, 2) }

  # deliberate failures
  specify { expect { |b| array.each(&b) }.not_to yield_successive_args(1, 2, 3) }
  specify { expect { |b| array_of_tuples.each(&b) }.not_to yield_successive_args([:a, :b], [:c, :d]) }
  specify { expect { |b| array.each(&b) }.not_to yield_successive_args(Integer, Integer, Integer) }
  specify { expect { |b| array.each(&b) }.to yield_successive_args(1, 2) }
end

When I run rspec yield_successive_args_spec.rb

Then the output should contain all of these:

8 examples, 4 failures
expected given block not to yield successively with arguments, but yielded with expected arguments
expected given block to yield successively with arguments, but yielded with unexpected arguments