Skip to content

Instantly share code, notes, and snippets.

@myronmarston
Last active August 29, 2015 14:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save myronmarston/6fd8a29944009db64455 to your computer and use it in GitHub Desktop.
Save myronmarston/6fd8a29944009db64455 to your computer and use it in GitHub Desktop.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment