Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

Better array assertions using collect

You could do this:

Person.tall_people.length.should == 3
Person.tall_people[0].should == people(:linda)
Person.tall_people[1].should == people(:dwane)
Person.tall_people[2].should == people(:rick)

This is better, because it’s clearer, and because in one stroke you prove bounds, content, and order:

Person.tall_people.should == [

I argue that this is best:

Person.tall_people.collect{|p|p.first_name}.should == [

Failures messages are a pleasure to read

  expected: ["Linda", "Dwane", "Rick"],
  got: ["Juliette", "Jeanne"] (using ==)

And code is clearer overall (the price is a “collect”)

Collecting away from the original object (and into primitives) is not only clearer, in most cases your aim is not to re-prove that the element objects are fully and properly configured. You’ve already done that elsewhere. You only want to prove, in the simplest possible terms, that the group of things you got is what you expected to get.

Let’s say you don’t intend to care about order. Sorting on primitives is a snap:

Person.tall_people.collect{|p|p.first_name}.sort.should == [

Slightly more controversial:

Person.short_people.collect{|p|p.first_name}.should == []

The collect seems silly at first glance, but you’re making present and future assertion failures much friendlier. You’ll be happy about brain cycles saved and sanity kept during big refactorings.

  1. Pat Maddox says:

    *Pat #2 tags in*

    I’ve been thinking about this off and on for a while. Tonight I finally came up with a way I like:

    [1,2,3].should == unordered(3,1,2)

    What do you guys think? It’s just shoulda’s assert_same_elements. Code is at

  2. Pat Maddox says:

    > Granted, ActiveRecord objects can lead to some gnarly test failure messages, but that should be no reason to change how you write your tests

    I don’t agree with that. Steve makes a very interesting point about slightly sacrificing code clarity in favor of clarity in the failure messages. I think it should be taken a bit further though. Instead of choosing between gnarly failure messages and noisy examples, you should improve the given failure messages, or build a clear abstraction over the ugly code that provides pretty failures. Improving the generated messages is more robust, but also more work, because you’d have to create special formatters and handle several conditions. Abstracting the existing mechanism is simple to implement, but because the underlying code was not written with this abstraction in mind, it can evolve independently and no longer be sufficient for the abstracted expression. But I’d say that for stuff like RSpec’s operator matchers, which are stable, it’s an efficient technique for improving the test over all.

  3. Brian Takita says:

    @Pat – Aww, you beat me to it. On Casebook, we added the include_all matcher.

    [1,2,3].should include_all(3,1,2)

    The code is not available publicly, at this time. We also made the error message more explicit.

  4. Karl says:

    You are missing the even more controversial:

    Person.short_people.collect{|p|p.reason_to_live}.should == []

    Sorry, I just couldn’t resist. Gotta love Randy Newman.

  5. Pat Maddox says:

    @Brian – The thing I don’t like about that is that I would expect

    [1,2,3,1].should include_all(3,1,2)

    to be true. But that’s not the behavior we’re going for in this case. unordered is the best I’ve been able to come up with, but David doesn’t seem to like the name.

  6. Adam Milligan says:

    I have to say I agree pretty strongly with Pat N on this. Not only does the collect change what the test communicates, but it also actually tests the wrong comparisons. Objects define their own meaning for comparison, which we should respect.

    Person.tall_people should return a collection containing Person objects. To verify that this contains the appropriate people, we should compare to an expected list of Person objects, using Person#== (or, perversely, Person#eql?). Comparing attributes of the expected objects, which have a different type and therefore potentially different comparison semantics, opens the door for incongruities.

    This is a subtle point, and one not likely to come up, but I think it mirrors the conceptual disconnect that this example creates.

    All of this assumes, of course, that you can guarantee that Person#first_name returns (and will always return) a unique and non-nil value for each individual Person instance.

  7. Jeff Dean says:

    Hopefully better array matching will be a part of rspec soon – you can follow the progress at [the lighthouse ticket](

  8. Jim Kingdon says:

    Adam: my biggest problem with relying on == (or equals in java, or similar mechanisms) is that many applications have at least two interesting notions of object “equality”. (When working on mayfly this is particularly amusing as one of the kinds of equality, SQL =, isn’t even reflexive).

    Trying to have inspect (or toString in java, or the like) do the right thing in all contexts has somewhat similar problems, although perhaps not as severe (especially in ruby which has both to_s and inspect).

    The extra collect doesn’t really bother me, but I guess if there was another way of getting good failure messages I suppose I could live with that.

  9. Adam Milligan says:

    @Jim: Inconsistent object equality is a more fundamental problem than good or bad test failure messages; it’s a problem with your domain model. Solve the problem, not the symptom. I realize there are, as in all things, instances in which you just have to live with suboptimal code, but basing a general principle specific instances of brokenness leads to madness.

    However, on a vaguely related note, this brings up one of the, in my opinion, major flaws in Ruby: the difference between #equal?, #eql?, and #==. Sure, they’re spelled differently, but they’re all pronounced the same; two mean object equality, one means pointer equality (yes, Ruby uses pointer semantics, no matter what you call it). How many people know which is which?

  10. Steve Conover says:

    “Not only does the collect change what the test communicates, but it also actually tests the wrong comparisons. Objects define their own meaning for comparison, which we should respect.”

    I will disagree head-on with this statement. I don’t accept that a method we’ve come to call “equals” or “==” or whatever is superior to “first_name” in this situation.

    I care about establishing that the identity of the object is the same, and I want my failure messages to read nicely. I don’t want all equality to be based on first_name for obvious reasons, but local to this test I can assume first_name’s are unique.

    Or to put it another way, I wholeheartedly accept critiques on style (as many commenters have put forward). Critiques based on some presumed deeper normative rules are just taste preferences in disguise. There’s nothing about the effectiveness of the test that changes by using object equality. (I actually think this is an important point. A better test does not over-prove: it proves the intended outcomes as precisely as possible and tries not to fail for reasons unimportant in that context).

  11. Adam Milligan says:

    Good tests don’t over-prove, but nor do they under-prove. This code:

    Person.tall_people.collect{|p|p.first_name}.should == [“Linda”]

    is meaningless if your database contains ten people with the first name Linda, only one of which is tall. Each Person object is distinct, and will describe itself as distinct from others when asked appropriately, but the first_name comparison will fail to draw that distinction.

    This may seem over-cautious, but who hasn’t had fixture data change under their feet?

  12. Pat Nakajima says:

    Just for fun, a ridiculously overreaching yet minimally applicable solution:

    class Person
    class << self attr_accessor :tall_people end self.tall_people = [] attr_reader :name def initialize(name=nil) @name = name end end require ‘rubygems’ require ‘spec’ describe Person do attr_reader :timmy, :linda before(:all) do Object.class_eval do def linda? name == ‘Linda’ rescue false end def has_linda? any? { |person| person.linda? } end end end before(:each) do @timmy =“Pat”) @linda =“Linda”) end it “knows if person is linda” do timmy.should_not be_linda linda.should be_linda end it “knows if linda is in collection” do Person.tall_people.should_not have_linda Person.tall_people << linda Person.tall_people.should have_linda end end

Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *