Skip to content

Instantly share code, notes, and snippets.

@myronmarston
Last active August 29, 2015 14:15
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/d0c3dbd1b88e9317c8e7 to your computer and use it in GitHub Desktop.
Save myronmarston/d0c3dbd1b88e9317c8e7 to your computer and use it in GitHub Desktop.

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:

@nurmuhammadsirat
Copy link

Hi, @geekmat here.

Haven't dived into RSpec but is it not possible for expect to accept
either args or blocks, and process them internally?

What I meant was to either use args or blocks exclusively, and not both. So we either do expect( ... ).to or expect { ... }.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!

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