Sorry, I forgot to formulate what is on my mind: it seems to me that
chained `change` matcher share the "actual" value or at least evaluate it
only once. Couldn't another 3rd-party matcher `pass` (or whatever) be part
of that chain? Or, to put it the other way round, how do the `change`
matcher make sure they evaluate the assertion code only once?
On Sunday, December 26, 2021 at 10:09:18 AM UTC+1 Nick Sutterer wrote:
> Phil, thank you so much for your help! <3 This is really really
> appreciated!
>
> > 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.
>
> Yeah, but don't you think this will be confusing at some point? Seems like
> I might have to think about a different approach.
>
> Right now, I am wondering how RSpec power-users tackle this very common
> test case:
>
> 1. take a snapshot of ABC
> 2. run your test code
> 3. compare side-effects of ABC
> 4. test additional effects on D, ...
>
> I know from several consulting projects that people use something along
> the following lines.
> ```
> expect {
> expect(operation.call.success?).to eq true
> }.to change { A.count } .by(1)
> .and change { B.count } .by(1)
> .and change { C.count } .by(1)
>
> expect(D.last.status).to eq("active")
> # ...
> ```
>
> I'd love to have that look as follows.
>
> ```
> expect {
> operation.call # provides "actual"
> }.to pass # needs "actual"
> .and change { A.count } .by(1)
> .and change { B.count } .by(1)
> .and change { C.count } .by(1)
>
> expect(D.last.status).to eq("active")
> # ...
> ```
>
> However, we ran into the problems described above, not receiving the
> actual value, and so on. I'm a bit clueless right now as of I don't know if
> I am missing anything RSpec-y here (am I trying to do something "stupid"
> that RSpec is not designed for?) or is it RSpec that might benefit from the
> concept of *ordered* chained matchers that allow accessing actual after
> it has been evaluated?
>
> Thanks again!
> On Saturday, December 25, 2021 at 6:56:12 PM UTC+1 [email protected]
> wrote:
>
>> 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/5eb96e0a-2b71-474a-a052-7a2b43b17507n%40googlegroups.com.