Forum: RSpec Applying an rspec matcher against the elements of a collection

Posted by Pete Hodgson (Guest)
on 2010-02-23 19:27
(Received via mailing list)
Hi all,
I've tried to figure out whether rspec has any features to make it
easier to make assertions against the elements of a collection, but I
haven't had any luck finding anything so far. I thought I'd explain
the problem here, and propose a potential feature that might mitigate
it.

Let's say I have a Person class:

class Person < Struct.new( :name, :age )

  VOTING_AGE = 18
  def self.get_voters( people )
    people.reject{ |person| person.age < VOTING_AGE }
  end

end

As you can see we have a method here which filters a collection of
people, returning only those people old enough to vote. If I were to
test this method in rspec I might write:

describe 'Person vote filtering' do
  it 'filters out people younger than voting age' do
    people = [
      Person.new( 'jenny', 18 ),
      Person.new( 'dave', 12 ),
      Person.new( 'paul', 19 ),
      Person.new( 'lisa', 17 )
    ]

    voters = Person.get_voters( people )

    voter_names = voters.map{ |p| p.name }
    voter_names.should == ['jenny','paul']
  end
end

This works, but having to manually pull out the voter names into a
seperate collection just in order to check who was filtered and who
wasn't has always seemed clunky to me. What I would prefer is to be
able to check whether the collection contains person who matching my
expectations. Say I have a custom matcher:

Spec::Matchers.define :be_named do |expected|
  match do |actual|
    actual.name == expected
  end
end

Then I'd like to be able to write something like

voters.should( have(2).people )
voters.should( have_one_that( be_named('jenny') ) )
voters.should( have_one_that( be_named('paul') ) )

or even:

voters.should( have_elements_that(
  be_named( 'jenny' ),
  be_named( 'paul' )
)

To me this is a lot clearer - although the method names and how
they're composed into the DSL could clearly use some work ;).

Now, for the trivial case I've been using as an example it would
probably be overkill, but I often find myself writing fairly
convuluted code at the end of a test just to figure out whether a
collection contains an element that matches some complex predicate. It
seems to me that if rspec had the generic ability to apply matchers to
the elements of a collection it would raise the level of
expressiveness for this kind of tests.

Thoughts? Does Rspec already support something like this that I'm just
not aware of? If I were to write a patch implementing this would it
have any chance of being accepted?

Cheers,
Pete
Posted by Pat Maddox (Guest)
on 2010-02-23 22:44
(Received via mailing list)
I'm going to argue that your design is off, and then ignore the rest of 
your post :)

class Person < Struct.new(:name, :age)
  VOTING_AGE = 18

  def voter?
    age >= VOTING_AGE
  end
end

Now your tests become very simple:

Person.new('Jenny', 17).should_not be_voter
Person.new('Bob', 18).should be_voter

Why you want a Person.get_voters method to select voters from a list, 
I'm not really sure.  You can always just do:

voters = collection_of_people.select {|p| p.voter?}

Also, RSpec has two mechanisms for testing collections the way you want 
(so I guess I'm not ignoring your post after all).

If you only care about inclusion, you can use the include matcher:

jenny = Person.new('Jenny', 17)
bob = Person.new('Bob', 18)
sally = Person.new('Sally', 20)

voters = Person.get_voters(jenny, bob, sally)
voters.should include(bob, sally)
voters.should_not include(jenny)

there is also the set equality matcher, which checks that the contents 
of two collections are equal irrespective of order:

voters.should =~ [bob, sally]

Pat
Posted by Pete Hodgson (Guest)
on 2010-02-23 23:55
(Received via mailing list)
Pat, Thanks for the response.

On Feb 23, 1:43 pm, Pat Maddox <mailingli...@patmaddox.com> wrote:
> I'm going to argue that your design is off, and then ignore the rest of your post :)

Fair enough :) The 'design' in my example was made up on the spot to
try and illustrate the kind of issues I've been coming up against
without lots of irrelevant detail. I agree that the code has all sorts
of silly issues.

>
> Also, RSpec has two mechanisms for testing collections the way you want (so I guess I'm not ignoring your post after all).
>

Thanks for pointing me towards the include and =~ matchers. This was
part of what I was looking for, and would solve a lot of the issues
I've hit in the past.

That said, I still feel that it would be helpful to have some way of
applying a matcher against the elements of a collection. I will just
have to come up with a more plausible example... :)

Cheers,
Pete
Posted by Michael Guterl (mguterl)
on 2010-02-24 02:47
(Received via mailing list)
On Tue, Feb 23, 2010 at 4:43 PM, Pat Maddox <mailinglists@patmaddox.com> 
wrote:
<snip>
>
> there is also the set equality matcher, which checks that the contents of two collections are equal irrespective of order:
>
> voters.should =~ [bob, sally]
>

I can't believe I didn't know =~ could be used for comparing
collections regardless of order, this is awesome!

Thanks,
Michael Guterl
Posted by Pat Maddox (Guest)
on 2010-02-24 17:00
(Received via mailing list)
I missed one other one that you'll find handy.

voters.should have(2).items

Take a look at 
http://rspec.rubyforge.org/rspec/1.3.0/classes/Spec/Matchers.html 
because there's a lot of useful stuff...
Please log in before posting. Registration is free and takes only a minute.
Existing account (Switch to SSL-encrypted connection)
NEW: Do you have a Google/GoogleMail or Yahoo account? No registration required!
Log in with Google account | Log in with Yahoo account
No account? Register here.