Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow satisfy to match the block expectation return value #1477

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Enhancements:

* Improve the IO emulation in the output capture matchers (`output(...).to_stdout` et al)
by adding `as_tty` and `as_not_tty` to change the `tty?` flags. (Sergio Gil Pérez de la Manga, #1459)
* Allow `satisfy` to match block expectations return value. (Phil Pirozhkov, #1477)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Allow `satisfy` to match block expectations return value. (Phil Pirozhkov, #1477)
* Improve the `satisfy` matcher to allow assertations on an `expect { ... }` return value.
(Phil Pirozhkov, #1477)


### 3.13.1 / 2024-06-13
[Full Changelog](http://github.com/rspec/rspec-expectations/compare/v3.13.0...v3.13.1)
Expand Down
31 changes: 31 additions & 0 deletions features/built_in_matchers/satisfy.feature
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ Feature: `satisfy` matcher
end
```

The `satisfy` matcher can also be used on block expectations to match the returned value of the block:

```ruby
expect { 2 + 2 }.to satisfy { |returned_value| returned_value == 4 }
```

This comes handy to check both the side effects of the subject under test and its returned value:

```ruby
expect { request! }
.to change { Log.count }.by(1)
.and satisfy { |response| response.success? }
```

@skip-when-ripper-unsupported
Scenario: Basic usage
Given a file named "satisfy_matcher_spec.rb" with:
Expand All @@ -39,3 +53,20 @@ Feature: `satisfy` matcher
| expected 10 to satisfy expression `v > 15` |
| expected 10 not to be greater than 5 |
| expected 10 to be greater than 15 |

@skip-when-ripper-unsupported
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you just copy this tag or check to see if it works without it? I'd expect it to work without it as its not matching source.

Scenario: Matching the block expectation return value
Given a file named "satisfy_matcher_returned_value_spec.rb" with:
"""ruby
RSpec.describe "double-purpose" do
it "adds an entry and returns the sum" do
ary = [1, 2, 3]
expect { ary.shift }
.to change { ary }.to([2, 3])
.and satisfy { |returned_value| returned_value == 1 }
end
end
"""
When I run `rspec satisfy_matcher_returned_value_spec.rb`
Then the output should contain all of these:
| 1 example, 0 failures |
9 changes: 7 additions & 2 deletions lib/rspec/matchers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -795,8 +795,9 @@ def respond_to(*names)
alias_matcher :an_object_responding_to, :respond_to
alias_matcher :responding_to, :respond_to

# Passes if the submitted block returns true. Yields target to the
# block.
# Passes if the submitted block returns true.
# For value expectations, yields target to the block.
# For block expectations, yields the expectation's returned value to the block.
Comment on lines +798 to +800
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Passes if the submitted block returns true.
# For value expectations, yields target to the block.
# For block expectations, yields the expectation's returned value to the block.
# Passes if the submitted block returns true. Yields targer to the
# block. This is either the direct value or the result of an `expect` block.

#
# Generally speaking, this should be thought of as a last resort when
# you can't find any other way to specify the behaviour you wish to
Expand All @@ -810,6 +811,10 @@ def respond_to(*names)
# @example
# expect(5).to satisfy { |n| n > 3 }
# expect(5).to satisfy("be greater than 3") { |n| n > 3 }
#
# expect { ary.shift }
# .to change { ary }.to(be_empty)
# .and satisfy { |returned_value| returned_value == :last_on_the_list }
def satisfy(description=nil, &block)
BuiltIn::Satisfy.new(description, &block)
end
Expand Down
7 changes: 6 additions & 1 deletion lib/rspec/matchers/built_in/compound.rb
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,13 @@ def initialize(actual, matcher_1, matcher_2)

inner, outer = order_block_matchers

returned = nil
@match_results[outer] = outer.matches?(Proc.new do |*args|
@match_results[inner] = inner.matches?(inner_matcher_block(args))
p = Proc.new { |*args2|
returned = inner_matcher_block(args).call(*args2)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to take care of ruby2 keywords (e.g. marked as such, not having keywords itself) so that they get passed through, I'm also a bit nervous about returned why isn't it just implict from the assignment?

@match_results[inner] = inner.matches?(p)
returned
end)
end

Expand Down
18 changes: 15 additions & 3 deletions lib/rspec/matchers/built_in/satisfy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@ def initialize(description=nil, &block)

# @private
def matches?(actual, &block)
@block = block if block
@actual = actual
@block.call(actual)
if Proc === actual
@actual = actual.call
@block.call(@actual)
else
@block = block if block
@actual = actual
@block.call(actual)
end
end

# @private
Expand All @@ -34,6 +39,13 @@ def failure_message_when_negated
"expected #{actual_formatted} not to #{description}"
end

# @api private
# Indicates this matcher matches against a block.
# @return [True]
def supports_block_expectations?
true
end

private

if RSpec::Support::RubyFeatures.ripper_supported?
Expand Down
2 changes: 1 addition & 1 deletion spec/rspec/matchers/aliased_matcher_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def description
end
RSpec::Matchers.alias_matcher :alias_of_my_base_matcher, :my_base_matcher

it_behaves_like "an RSpec value matcher", :valid_value => 13, :invalid_value => nil do
it_behaves_like "an RSpec value-only matcher", :valid_value => 13, :invalid_value => nil do
let(:matcher) { alias_of_my_base_matcher }
end

Expand Down
2 changes: 1 addition & 1 deletion spec/rspec/matchers/built_in/all_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module RSpec::Matchers::BuiltIn
RSpec.describe All do

it_behaves_like 'an RSpec value matcher', :valid_value => ['A', 'A', 'A'], :invalid_value => ['A', 'A', 'B'], :disallows_negation => true do
it_behaves_like 'an RSpec value-only matcher', :valid_value => ['A', 'A', 'A'], :invalid_value => ['A', 'A', 'B'], :disallows_negation => true do
let(:matcher) { all( eq('A') ) }
end

Expand Down
2 changes: 1 addition & 1 deletion spec/rspec/matchers/built_in/be_between_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def inspect
end
end

it_behaves_like "an RSpec value matcher", :valid_value => (10), :invalid_value => (11) do
it_behaves_like "an RSpec value-only matcher", :valid_value => (10), :invalid_value => (11) do
let(:matcher) { be_between(1, 10) }
end

Expand Down
2 changes: 1 addition & 1 deletion spec/rspec/matchers/built_in/be_instance_of_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module RSpec
module Matchers
[:be_an_instance_of, :be_instance_of].each do |method|
RSpec.describe "expect(actual).to #{method}(expected)" do
it_behaves_like "an RSpec value matcher", :valid_value => "a", :invalid_value => 5 do
it_behaves_like "an RSpec value-only matcher", :valid_value => "a", :invalid_value => 5 do
let(:matcher) { send(method, String) }
end

Expand Down
2 changes: 1 addition & 1 deletion spec/rspec/matchers/built_in/be_kind_of_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module RSpec
module Matchers
[:be_a_kind_of, :be_kind_of].each do |method|
RSpec.describe "expect(actual).to #{method}(expected)" do
it_behaves_like "an RSpec value matcher", :valid_value => 5, :invalid_value => "a" do
it_behaves_like "an RSpec value-only matcher", :valid_value => 5, :invalid_value => "a" do
let(:matcher) { send(method, Integer) }
end

Expand Down
2 changes: 1 addition & 1 deletion spec/rspec/matchers/built_in/be_within_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module RSpec
module Matchers
RSpec.describe "expect(actual).to be_within(delta).of(expected)" do
it_behaves_like "an RSpec value matcher", :valid_value => 5, :invalid_value => -5 do
it_behaves_like "an RSpec value-only matcher", :valid_value => 5, :invalid_value => -5 do
let(:matcher) { be_within(2).of(4.0) }
end

Expand Down
8 changes: 4 additions & 4 deletions spec/rspec/matchers/built_in/compound_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -262,12 +262,12 @@ def expect_block

describe "expect(...).to matcher.and(other_matcher)" do

it_behaves_like "an RSpec value matcher", :valid_value => 3, :invalid_value => 4, :disallows_negation => true do
it_behaves_like "an RSpec value-only matcher", :valid_value => 3, :invalid_value => 4, :disallows_negation => true do
let(:matcher) { eq(3).and be <= 3 }
end

context 'when using boolean AND `&` alias' do
it_behaves_like "an RSpec value matcher", :valid_value => 3, :invalid_value => 4, :disallows_negation => true do
it_behaves_like "an RSpec value-only matcher", :valid_value => 3, :invalid_value => 4, :disallows_negation => true do
let(:matcher) { eq(3) & be_a(Integer) }
end
end
Expand Down Expand Up @@ -662,12 +662,12 @@ def actual
end

describe "expect(...).to matcher.or(other_matcher)" do
it_behaves_like "an RSpec value matcher", :valid_value => 3, :invalid_value => 5, :disallows_negation => true do
it_behaves_like "an RSpec value-only matcher", :valid_value => 3, :invalid_value => 5, :disallows_negation => true do
let(:matcher) { eq(3).or eq(4) }
end

context 'when using boolean OR `|` alias' do
it_behaves_like "an RSpec value matcher", :valid_value => 3, :invalid_value => 5, :disallows_negation => true do
it_behaves_like "an RSpec value-only matcher", :valid_value => 3, :invalid_value => 5, :disallows_negation => true do
let(:matcher) { eq(3) | eq(4) }
end
end
Expand Down
2 changes: 1 addition & 1 deletion spec/rspec/matchers/built_in/contain_exactly_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ def array.send; :sent; end
end

RSpec.describe "expect(array).to contain_exactly(*other_array)" do
it_behaves_like "an RSpec value matcher", :valid_value => [1, 2], :invalid_value => [1] do
it_behaves_like "an RSpec value-only matcher", :valid_value => [1, 2], :invalid_value => [1] do
let(:matcher) { contain_exactly(2, 1) }
end

Expand Down
2 changes: 1 addition & 1 deletion spec/rspec/matchers/built_in/cover_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
if (1..2).respond_to?(:cover?)
RSpec.describe "expect(...).to cover(expected)" do
it_behaves_like "an RSpec value matcher", :valid_value => (1..10), :invalid_value => (20..30) do
it_behaves_like "an RSpec value-only matcher", :valid_value => (1..10), :invalid_value => (20..30) do
let(:matcher) { cover(5) }
end

Expand Down
2 changes: 1 addition & 1 deletion spec/rspec/matchers/built_in/eq_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module RSpec
module Matchers
RSpec.describe "eq" do
it_behaves_like "an RSpec value matcher", :valid_value => 1, :invalid_value => 2 do
it_behaves_like "an RSpec value-only matcher", :valid_value => 1, :invalid_value => 2 do
let(:matcher) { eq(1) }
end

Expand Down
2 changes: 1 addition & 1 deletion spec/rspec/matchers/built_in/eql_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module RSpec
module Matchers
RSpec.describe "eql" do
it_behaves_like "an RSpec value matcher", :valid_value => 1, :invalid_value => 2 do
it_behaves_like "an RSpec value-only matcher", :valid_value => 1, :invalid_value => 2 do
let(:matcher) { eql(1) }
end

Expand Down
2 changes: 1 addition & 1 deletion spec/rspec/matchers/built_in/equal_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module RSpec
module Matchers
RSpec.describe "equal" do
it_behaves_like "an RSpec value matcher", :valid_value => :a, :invalid_value => :b do
it_behaves_like "an RSpec value-only matcher", :valid_value => :a, :invalid_value => :b do
let(:matcher) { equal(:a) }
end

Expand Down
2 changes: 1 addition & 1 deletion spec/rspec/matchers/built_in/exist_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
RSpec.describe "exist matcher" do
it_behaves_like "an RSpec value matcher", :valid_value => Class.new { def exist?; true; end }.new,
it_behaves_like "an RSpec value-only matcher", :valid_value => Class.new { def exist?; true; end }.new,
:invalid_value => Class.new { def exist?; false; end }.new do
let(:matcher) { exist }
end
Expand Down
2 changes: 1 addition & 1 deletion spec/rspec/matchers/built_in/has_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
RSpec.describe "expect(...).to have_sym(*args)" do
it_behaves_like "an RSpec value matcher", :valid_value => { :a => 1 },
it_behaves_like "an RSpec value-only matcher", :valid_value => { :a => 1 },
:invalid_value => {} do
let(:matcher) { have_key(:a) }
end
Expand Down
4 changes: 2 additions & 2 deletions spec/rspec/matchers/built_in/have_attributes_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def respond_to?(method_name)
end

describe "expect(...).to have_attributes(with_one_attribute)" do
it_behaves_like "an RSpec value matcher", :valid_value => Person.new("Correct name", 33), :invalid_value => Person.new("Wrong Name", 11) do
it_behaves_like "an RSpec value-only matcher", :valid_value => Person.new("Correct name", 33), :invalid_value => Person.new("Wrong Name", 11) do
let(:matcher) { have_attributes(:name => "Correct name") }
end

Expand Down Expand Up @@ -148,7 +148,7 @@ def count
end

describe "expect(...).to have_attributes(with_multiple_attributes)" do
it_behaves_like "an RSpec value matcher", :valid_value => Person.new("Correct name", 33), :invalid_value => Person.new("Wrong Name", 11) do
it_behaves_like "an RSpec value-only matcher", :valid_value => Person.new("Correct name", 33), :invalid_value => Person.new("Wrong Name", 11) do
let(:matcher) { have_attributes(:name => "Correct name", :age => 33) }
end

Expand Down
2 changes: 1 addition & 1 deletion spec/rspec/matchers/built_in/include_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def hash.send; :sent; end
end

describe "expect(...).to include(with_one_arg)" do
it_behaves_like "an RSpec value matcher", :valid_value => [1, 2], :invalid_value => [1] do
it_behaves_like "an RSpec value-only matcher", :valid_value => [1, 2], :invalid_value => [1] do
let(:matcher) { include(2) }
end

Expand Down
2 changes: 1 addition & 1 deletion spec/rspec/matchers/built_in/match_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
RSpec.describe "expect(...).to match(expected)" do
include RSpec::Support::Spec::DiffHelpers

it_behaves_like "an RSpec value matcher", :valid_value => 'ab', :invalid_value => 'bc' do
it_behaves_like "an RSpec value-only matcher", :valid_value => 'ab', :invalid_value => 'bc' do
let(:matcher) { match(/a/) }
end

Expand Down
2 changes: 1 addition & 1 deletion spec/rspec/matchers/built_in/respond_to_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
RSpec.describe "expect(...).to respond_to(:sym)" do
it_behaves_like "an RSpec value matcher", :valid_value => "s", :invalid_value => 5 do
it_behaves_like "an RSpec value-only matcher", :valid_value => "s", :invalid_value => 5 do
let(:matcher) { respond_to(:upcase) }
end

Expand Down
51 changes: 51 additions & 0 deletions spec/rspec/matchers/built_in/satisfy_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,54 @@
end
end
end

RSpec.describe "expect { ... }.to satisfy { block }" do
it_behaves_like "an RSpec block matcher", :disallows_negation => true, :skip_deprecation_check => true do
let(:matcher) { satisfy { |v| v == 1 } }
before { @k = 0 }
def valid_block
1
end
def invalid_block
2
end
end

let(:ary) { [1, 2] }

it "matches the returned value" do
expect { ary.shift }.to satisfy { |returned_value| returned_value == 1 }
end

it "provides a sensible failure message", :skip => !RSpec::Support::RubyFeatures.ripper_supported? do
expect {
expect { ary.shift }.to satisfy { |returned_value| returned_value == :other }
}.to fail_with("expected 1 to satisfy expression `returned_value == :other`")
end

context "when negated" do
it "passes when the returned value doesn't match" do
expect { ary.shift }.not_to satisfy { |returned_value| returned_value == 2 }
end

it "fails when the retuned value matches", :skip => !RSpec::Support::RubyFeatures.ripper_supported? do
expect {
expect { ary.shift }.not_to satisfy { |returned_value| returned_value == 1 }
}.to fail_with("expected 1 not to satisfy expression `returned_value == 1`")
end
end

describe "composed usage" do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should follow on from the example group description

Suggested change
describe "composed usage" do
describe "when composed with other matchers" do

it "works as a root matcher" do
expect { ary.shift }.to satisfy { |returned_value| returned_value == 1 }.and change { ary }.to([2])
end

it "works as a supplemental matcher" do
expect { ary.shift }.to change { ary }.to([2]).and satisfy { |returned_value| returned_value == 1 }
end
end

pending "allows a matcher as an argument" do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not introduce pending specs unless its for something version specific, what needs to be done to make this work?

expect { ary.shift }.to satisfy(eq(2))
end
end
4 changes: 2 additions & 2 deletions spec/rspec/matchers/built_in/start_and_end_with_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
RSpec.describe "expect(...).to start_with" do
it_behaves_like "an RSpec value matcher", :valid_value => "ab", :invalid_value => "bc" do
it_behaves_like "an RSpec value-only matcher", :valid_value => "ab", :invalid_value => "bc" do
let(:matcher) { start_with("a") }
end

Expand Down Expand Up @@ -207,7 +207,7 @@ def ==(other)
end

RSpec.describe "expect(...).to end_with" do
it_behaves_like "an RSpec value matcher", :valid_value => "ab", :invalid_value => "bc" do
it_behaves_like "an RSpec value-only matcher", :valid_value => "ab", :invalid_value => "bc" do
let(:matcher) { end_with("b") }
end

Expand Down
2 changes: 1 addition & 1 deletion spec/rspec/matchers/define_negated_matcher_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def description
include_examples "making a copy", :clone

RSpec::Matchers.define_negated_matcher :an_array_excluding, :include
it_behaves_like "an RSpec value matcher", :valid_value => [1, 3], :invalid_value => [1, 2] do
it_behaves_like "an RSpec value-only matcher", :valid_value => [1, 3], :invalid_value => [1, 2] do
let(:matcher) { an_array_excluding(2) }
end

Expand Down
2 changes: 1 addition & 1 deletion spec/rspec/matchers/dsl_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def new_matcher(name, *expected, &block)
RSpec::Matchers::DSL::Matcher.new(name, block, self, *expected)
end

it_behaves_like "an RSpec value matcher", :valid_value => 1, :invalid_value => 2 do
it_behaves_like "an RSpec value-only matcher", :valid_value => 1, :invalid_value => 2 do
let(:matcher) do
new_matcher(:equal_to_1) do
match { |v| v == 1 }
Expand Down
Loading
Loading