9

I want to test an iterator using rspec. It seems to me that the only possible yield matcher is yield_successive_args (according to https://www.relishapp.com/rspec/rspec-expectations/v/3-0/docs/built-in-matchers/yield-matchers). The other matchers are used only for single yielding.

But yield_successive_args fails if the yielding is in other order than specified.

Is there any method or nice workaround for testing iterator that yields in any order?

Something like the following:

expect { |b| array.each(&b) }.to yield_multiple_args_in_any_order(1, 2, 3)
mirelon
  • 4,896
  • 6
  • 40
  • 70
  • I added a feature request, feel free to suggest a better name than `yield_multiple_args`: https://github.com/rspec/rspec-expectations/issues/595 – mirelon Jul 01 '14 at 07:41
  • can you provide the iterator code here as well? – xlembouras Jul 26 '14 at 07:53
  • I am looking for a general solution for any iterator, the only thing that matters is that it yields all params in any order. – mirelon Jul 28 '14 at 09:40

2 Answers2

3

Here is the matcher I came up for this problem, it's fairly simple, and should work with a good degree of efficiency.

require 'set'

RSpec::Matchers.define :yield_in_any_order do |*values|
  expected_yields = Set[*values]
  actual_yields = Set[]

  match do |blk|
    blk[->(x){ actual_yields << x }]    # ***
    expected_yields == actual_yields    # ***
  end

  failure_message do |actual|
    "expected to receive #{surface_descriptions_in expected_yields} "\
    "but #{surface_descriptions_in actual_yields} were yielded."
  end

  failure_message_when_negated do |actual|
    "expected not to have all of "\
    "#{surface_descriptions_in expected_yields} yielded."
  end

  def supports_block_expectations?
    true
  end
end

I've highlighted the lines containing most of the important logic with # ***. It's a pretty straightforward implementation.

Usage

Just put it in a file, under spec/support/matchers/, and make sure you require it from the specs that need it. Most of the time, people just add a line like this:

Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f}

to their spec_helper.rb but if you have a lot of support files, and they aren't all needed everywhere, this can get a bit much, so you may want to only include it where it is used.

Then, in the specs themselves, the usage is like that of any other yielding matcher:

class Iterator
  def custom_iterator
    (1..10).to_a.shuffle.each { |x| yield x }
  end
end

describe "Matcher" do
  it "works" do
    iter = Iterator.new
    expect { |b| iter.custom_iterator(&b) }.to yield_in_any_order(*(1..10))
  end
end
amnn
  • 3,657
  • 17
  • 23
  • For reference, there is an issue on github: https://github.com/rspec/rspec-expectations/pull/595 and pull request: https://github.com/rspec/rspec-expectations/pull/596. Maybe when there is more demand, it will make it's way to the codebase. – mirelon Aug 01 '14 at 09:09
0

This can be solved in plain Ruby using a set intersection of arrays:

array1 = [3, 2, 4]
array2 = [4, 3, 2]
expect(array1).to eq (array1 & array2)

# for an enumerator:
enumerator = array1.each
expect(enumerator.to_a).to eq (enumerator.to_a & array2)

The intersection (&) will return items that are present in both collections, keeping the order of the first argument.

chipairon
  • 2,031
  • 2
  • 19
  • 21
  • What about `array1 = [3, 2, 4]; array2 = [4, 3, 2, 1]` ? I suggest here the `match_array` matcher would be more fit when comparing arrays. – mirelon Jul 28 '14 at 09:49
  • This can lead to a quazi-correct answer when there is an iterator with method `each` and you use `iterator.to_enum.to_a` with a `match_array` matcher. But the question was about yielding of any iterator method, not providing correct `to_enum` method. – mirelon Aug 01 '14 at 09:17