i used to using expect(subject/double).to haved_received(:a_method).with(args).exactly(n).times
to test that a method be called with some specific arguments and be called exactly {n} times. But today it's broken with arguments are Comparable
objects, take a look at the below code:
setup
class A; end
class B
include Comparable
attr_reader :val
def initialize(val)
@val = val
end
def <=>(other)
self.val <=> other.val
end
end
class S
def call(x); end
end
s = S.new
allow(s).to receive(:call)
now the below test passed with normal object A
a1 = A.new
a2 = A.new
s.call(a1)
s.call(a2)
expect(s).to have_received(:call).with(a1).exactly(1).times
expect(s).to have_received(:call).with(a2).exactly(1).times
but it failed with Comparable
object B
b1 = B.new(0)
b2 = B.new(0)
s.call(b1)
s.call(b2)
expect(s).to have_received(:call).with(b1).exactly(1).times
expect(s).to have_received(:call).with(b2).exactly(1).times
i debug and saw that the rspec matcher call the spaceship operator <=>
to verify arguments, so it considers b1 and b2 are the same
Failure/Error: expect(s).to have_received(:call).with(b1).exactly(1).times
expected: 1 time with arguments:
received: 2 times with arguments:
What should i do to pass the test ?
This happens because Comparable
implements ==
, so your objects are treated as being equal in regards to ==
:
b1 = B.new(0)
b2 = B.new(0)
b1 == b2 #=> true
To set a constraint based on object identity, you can use the equal
matcher: (or its aliases an_object_equal_to
/ equal_to
)
expect(s).to have_received(:call).with(an_object_equal_to(b1)).once
Under the hood, this matcher calls equal?
:
b1 = B.new(0)
b2 = B.new(0)
b1.equal?(b2) #=> false