I recently started a new project using rspec's newer (and soon to be default) expect
syntax, and encountered this error:
expect(5).to == 5
ArgumentError: The expect syntax does not support operator matchers, so you must pass a matcher to `#to`
"Why'd they take out operator matches? I've grown quite accustomed to them!", I thought. Digging around, the source of this change started in pull request 119 citing issue 138 as one of the root causes. Here's what's actually happening:
5.should == 5
actually evaluates to (5.should).==(5)
. Using pry
from an example:
[35] pry(#<RSpec::Core::ExampleGroup::Nested_1::Nested_1>)> 5.should
=> #<RSpec::Matchers::BuiltIn::PositiveOperatorMatcher:0x00000002fb9c38 @actual=5>
[36] pry(#<RSpec::Core::ExampleGroup::Nested_1::Nested_1>)> ls _
RSpec::Matchers::OperatorMatcher#methods: != !~ < <= == === =~ > >= description fail_with_message
RSpec::Matchers::BuiltIn::PositiveOperatorMatcher#methods: __delegate_operator
instance variables: @actual
On the other hand, the preferred route of 5.should eq(5)
actually evaluates to (5.should(eq(5))
. From pry
:
[37] pry(#<RSpec::Core::ExampleGroup::Nested_1::Nested_1>)> eq(5)
=> #<RSpec::Matchers::BuiltIn::Eq:0x0000000308ba58 @expected=5>
[38] pry(#<RSpec::Core::ExampleGroup::Nested_1::Nested_1>)> 5.should(_)
=> true
Spot the difference? With ==, should
is called with no arguments, and the operator is run on the object returned by should
. With eq
, a matcher is instantiated and passed as the first argument to should
.
Why is this an issue? Look at that issue 138, and you'll see that it's plausibly easy to accidentally break your assertion onto two lines, such that no actual assertion happens:
[41] pry(#<RSpec::Core::ExampleGroup::Nested_1::Nested_1>)> 5.should
=> #<RSpec::Matchers::BuiltIn::PositiveOperatorMatcher:0x0000000347a0d8 @actual=5>
[42] pry(#<RSpec::Core::ExampleGroup::Nested_1::Nested_1>)> eq(4)
=> #<RSpec::Matchers::BuiltIn::Eq:0x00000002a74570 @expected=4>
With the expect
syntax, it was decided to make the .to method require a matcher object to be passed in as a parameter. Stand alone operator matches no longer are viable because operator methods need an explicit receiver.
[44] pry(#<RSpec::Core::ExampleGroup::Nested_1::Nested_1>)> def eq(value)
[44] pry(#<RSpec::Core::ExampleGroup::Nested_1::Nested_1>)* puts 'hi from the matcher'
[44] pry(#<RSpec::Core::ExampleGroup::Nested_1::Nested_1>)* end
=> nil
[45] pry(#<RSpec::Core::ExampleGroup::Nested_1::Nested_1>)> def ==(value)
[45] pry(#<RSpec::Core::ExampleGroup::Nested_1::Nested_1>)* puts 'hi from the matcher'
[45] pry(#<RSpec::Core::ExampleGroup::Nested_1::Nested_1>)* end
=> nil
[46] pry(#<RSpec::Core::ExampleGroup::Nested_1::Nested_1>)> eq(5)
hi from the matcher
=> nil
[47] pry(#<RSpec::Core::ExampleGroup::Nested_1::Nested_1>)> ==(5)
SyntaxError: unexpected tEQ, expecting $end
==(5)
^
If you want this to work, the expect-style call would have to read: expect(5).to self.==(5)
, and that's much harder to read and write than using the equivalent eq
, gt
, etc.
However, there's still a work around! Although "stand alone" operator matchers are out, the be
matcher has operator matches on it:
[48] pry(#<RSpec::Core::ExampleGroup::Nested_1::Nested_1>)> ls be
RSpec::Matchers::Pretty#methods: _pretty_print name name_to_sentence split_words to_sentence to_word underscore
RSpec::Matchers::BuiltIn::BaseMatcher#methods: actual description diffable? expected match_unless_raises matches? rescued_exception
RSpec::Matchers::BuiltIn::Be#methods: < <= == === =~ > >= failure_message_for_should failure_message_for_should_not match
So, even with the expect syntax, you can still write: expect(5).to be > 4
Thank you, this was helpful to me.