Haven't dived into RSpec but is it not possible for expect to accept either args or blocks, and process them internally?
First off, expect
does accept either an arg or a block already. But if
it's a block (or a proc/lambda arg), expect
does not call the block
automatically -- it just passes the block to the matcher and allows the
matcher to call it if it wants. This is necessary because some matchers
(the block matchers like raise_error
, change
, etc) must wrap the
block in some extra logic to work properly because they deal in side
effects, not expression return values.
As I said on Twitter, passing an arg is universally supported, as long
as you're willing to add the extra noise of proc { ... }
or lamdba { ... }
for the block matchers.
On the flip side, could we support passing a block universally? We've
thought about this and concluded that no, it's not possible to
support that universally without creating situations that are prone to
false positives, unless we wanted to change the matcher protocol (and
force all custom matchers to be rewritten) so that they have separate
matches_arg?
and matches_block?
methods -- but that isn't appealing
at all.
Consider this example:
class Foo
def self.maybe_nil
[nil, :not_nil].sample
end
end
expect { Foo.maybe_nil }.not_to be_nil
Here we've got a method (maybe_nil
) that returns nil
half the time,
randomly. This expectation expression, if we allowed it, would always pass, though.
To understand why, consider that passing a block to expect
is just
cleaner syntax for passing a lambda arg:
expect(lambda { Foo.maybe_nil }).not_to be_nil
...which can be rewritten just as a raw arg using a variable:
x = lambda { Foo.maybe_nil }
expect(x).not_to be_nil
Written like this, it's clear that this expectation will always pass:
x
is a lambda, and is not nil, so it must pass. However, in the
block form, what the user intended is that the Foo.maybe_nil
be
called, and the expectation be set on that method's return value.
But the block is never invoked (why would be_nil
invoke it? It
assumes nothing about the object it is given...it just checks if it is
nil, and it wasn't), so the expectation would always pass, regardless
of the contents of the block or lambda.
For more history and discussion on this see:
Hi, @geekmat here.
What I meant was to either use args or blocks exclusively, and not both. So we either do
expect( ... ).to
orexpect { ... }.to
.However, I think you have ninja-answered that question as well: some matchers check for return values, while others check for side effects.
Appreciate you taking the time to write this. Thanks for this!