Saturday, March 14, 2015

TMI from RSpec

   When using RSpec, there is a small annoyance waiting to getcha, when you compare numbers.

   TL;DR: using "be" (or "equal", which does exactly the same thing) produces distracting and usually meaningless extra verbiage, while "eq" (or "eql", which is a little different but close enough for this purpose) does not.

   Frex, if you are sending an HTTP request and expecting to get back the OK status code, you might say:

    expect(response.status).to be 200
   This will work fine, and so long as the status actually is 200, then all is right with the world.

   The minor, trivial, nitpicky, but still a bit annoying, problem, comes when the status is not 200.  Sure, you'll still get a test failure... but you'll get some added little distractions, the bold parts here:

    Failure/Error: expect(response.status).to be 200

       expected #<Fixnum:401> => 200
            got #<Fixnum:1001> => 500

       Compared using equal?, which compares object identity,
       but expected and actual are not the same object. Use
       `expect(actual).to eq(expected)` if you don't care about
       object identity in this example.
   All you really needed is this:

       expected: 200
            got: 500

       (compared using ==)
   (Okay, granted, you didn't really need to know it was compared using "==", but still, that's not too bad... at least it's a lot less distracting verbiage to wade through than the prior message.)

   (And okay, granted, four short lines of additional feedback aren't that much of a pain... but still, they add up.  If you're new on a project with lots of failing tests, as I am now, it can make it harder to wade through all the failures and see which ones actually mean anything.)

   So how do you get rid of it?  You might try using "==" instead of "be", especially if you're used to RSpec's older "should" syntax rather than the new "expect" syntax (which I'll ass-u-me you are now using)... but then you'd see:

     Failure/Error: expect(response.status).to == 200
     ArgumentError:
       The expect syntax does not support operator matchers, so you must pass a matcher to `#to`.
   But despair not, dear reader, all is not lost!  It's still very simple.  Instead of "be", use "eq".  Then you get:

     Failure/Error: expect(response.status).to eq 200

       expected: 200
            got: 500

       (compared using ==)
   As you may recall from my Ruby Gotchas presentation, there are several ways to compare things in Ruby:

  • "==" compares the value, without regard for the actual class(es) of the objects.  So, frex, "1.0 == 1" is true.  This is what RSpec uses when you say "eq" (or "==" in the older syntax); hence the message "(compared using ==)".

  • ".eql?" compares the value and class!  So, "1.0.eql? 1" is false!

  • ".equal?" is object identity.  That is, even if two things have the same value, and the same class, if they're not the exact same object, they are not ".equal?".  One of the oddities of the Ruby object system is that an object is created for each new unique Float value, so the result of:

        x = 1.0
        y = 1.0
        x.equal? y
    is indeed true!  However, if we use a more complex class, like String, we can see that "'foo'.equal? 'foo'" is false.
   You may now be wondering, what if you try "expect(response.status).to eql 200"?  Pretty much the same thing, just now it says "(compared using eql?)".

   But... if you take it one step further, and use "expect(response.status).to equal 200"... then you're back to Square One.  That shouldn't surprise any of you who paid close attention to the original verbose error message: it told you that when we used "be", it "Compared using equal?, which compares object identity".