Hi Nick!

> #1337

It's still up for the grabs ;)

> I'm referring to this issue:
https://github.com/rspec/rspec-expectations/issues/1336

>
> Basically, I'm trying to implement a matcher that can be chained with
> others after an `expect {}` , as in the following snippet.
>
> ```ruby
> expect { run({duration: "2.24"}) }
>       .to pass
>       .and change { "yo" }.by(1)
> ```
>
> When running this, I get this error:
> NoMethodError:
>        undefined method `success?' for #<Proc:0x000055697246eea8
> /home/nick/projects/rspec-trailblazer/spec/expectation_spec.rb:46>
>
> My matcher, when used in a chain, doesn't receive the `actual` result
> object (specific to `run` and our library) but a Proc instance. How would I
> write a matcher that supports the block syntax?
>

I believe the distinction is that block matchers check side effects, while
value matchers - returned value.
The documentation for building block matchers is here
https://relishapp.com/rspec/rspec-expectations/v/3-10/docs/custom-matchers/define-a-matcher-supporting-block-expectations
But I believe it's confusing in terms of telling a story how to create a
block matcher. Frankly, I can't create a usable composable block matcher
using those docs.

To add to the confusion, there are actually two ways of creating matchers.
The first one is with `RSpec::Matchers.define`, and another one is by
writing a regular class.
I usually start with `define`, get quickly frustrated and switch to a class.
One example is this matcher
https://github.com/pirj/rspec-enqueue_sidekiq_job/blob/main/lib/rspec/enqueue_sidekiq_job.rb#L73
But it's not checking the returned value along the way.

So I quickly came up with this:
```
$foo = 0

class Bar
  include RSpec::Matchers::Composable

  def initialize(expected_return_value)
    @expected_return_value = expected_return_value
  end

  def matches?(block)
    @actual_return_value = block.call

    values_match?(@expected_return_value, @actual_return_value)
  end

  def failure_message(*args)
    "Expected: #{@expected_return_value}, got: #{@actual_return_value}"
  end

  def supports_block_expectations?
    true
  end
end

module RSpec
  module Matchers # reopen for simplicity. you can `config.include
MyModule` in spec_helper
    def bar(expected_return_value)
      Bar.new(expected_return_value)
    end
  end
end

RSpec.describe "a custom block matcher" do
  specify do
    expect { $foo = 2 }
      .to bar(2)
      .and change { $foo }.by(2)
  end
end
```

and it works!

However, if you change the order:
```
    expect { $foo = 2 }
      .to change { $foo }.by(2)
      .and bar(2)
```
it starts complaining that `@actual_return_value` (the result of the block
being called) is `true`, not `2`.

It seems to me that only the innermost matcher can get the return value of
the block, and it's never passed over to composed matchers.

Can you work with that? This would imply that the matcher that checks the
return value is the always the first one in the chain.

It feels doable to pass the returned value of the block along.
We've restricted value matchers from working with block expectations in
RSpec 4, i.e.
```
expect { 1 }.to eq(1)
```
will raise an error.

But I have no certainty if this feature will not be confusing. Even though
it might be usable in some cases, e.g.:
```
expect { post :delete }
  .to be_ok
  .and change { User.count }.by(-1)
```
instead of
```
expect { post :delete }
  .to change { User.count }.by(-1)
expect(response).to be_ok
```

Let me know what you think.

> happy holidays

Happy holidays to you too, Nick!

- Phil

-- 
You received this message because you are subscribed to the Google Groups 
"rspec" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/rspec/CAAk5Ok8-O%3DExHSoib4YHwwn-9_GBLa8_xLxGcutkPk6Zjw-4kQ%40mail.gmail.com.

Reply via email to