The reason for this is that do...end
blocks bind at a different precedence level than {...}
blocks. Consider this expression:
expect { foo }.to change { x }
The { x }
block binds to change
, and is passed as a block to that method. However, if you use do...end
:
expect { foo }.to change do
x
end
...then the block binds to the to
method (ruby interprets it as expect { foo }.to(change) do x end
). In general, RSpec handles this by having the to
method forward any block it receives to matcher.matches?
when it calls that, so that for most matchers you could use do...end
. For example, this works because of the block forwarding behavior:
expect(object).to receive(:foo) do
:return_value
end
However, the change
matcher has one wrinkle that prevents us from being able to use that: it supports further chaining off the block:
expect { }.to change { x }.from(1).to(2)
expect { }.to change { x }.by(1)
expect { }.to change { x }.by_at_least(1)
# etc
These methods (from
, to
, by
, by_at_least
, etc) are defined on the change
matcher. We can't support them and allow the change
matcher to receive a do...end
block, because if we did, this is how Ruby would evaluate it:
# Given:
expect { foo }.to change do
x
end.by(1)
# ...this is what would happen:
expectation_target = expect { foo }
expectation_return_value = expectation_target.to(change) do
x
end
expectation_return_value.by(1) # => would raise a `NoMethodError`
# actually, expectations don't generally have a meaningful return value
# (they signal failure or success by raising an exception or not), but I've
# captured it in a local variable to demonstrate what happens here.
So, technically we could allow do...end
blocks as long as the user doesn't use any of the chainable fluent interface offered by the change
matcher. But that would be incredibly confusing for it to be supported until they user adds by
(or whatever) and then have it fail. And if we did support it, there would be no way to provide a good error message when they used do...end
and added by
, because the by
message gets sent to the return value of the expectation, not to the change
matcher.
Also, if you really want to use a do...end
block you actually can -- you just have to use parens to force ruby to bind the block to change
rather than to
:
x = 1
expect { x += 1 }.to(change do
x
end.by(1))
That expectation works, but users don't generally want to write their expectations like that.