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

Yield matchers #129

Merged
merged 17 commits into from
Apr 19, 2012
Merged
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
15 changes: 15 additions & 0 deletions README.md
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -112,6 +112,21 @@ expect { ... }.to throw_symbol(:symbol)
expect { ... }.to throw_symbol(:symbol, 'value') expect { ... }.to throw_symbol(:symbol, 'value')
``` ```


### Yielding

```ruby
expect { |b| 5.tap(&b) }.to yield_control # passes regardless of yielded args

expect { |b| yield_if_true(true, &b) }.to yield_with_no_args # passes only if no args are yielded

expect { |b| 5.tap(&b) }.to yield_with_args(5)
expect { |b| 5.tap(&b) }.to yield_with_args(Fixnum)
expect { |b| "a string".tap(&b) }.to yield_with_args(/str/)

expect { |b| [1, 2, 3].each(&b) }.to yield_successive_args(1, 2, 3)
expect { |b| { :a => 1, :b => 2 }.each(&b) }.to yield_successive_args([:a, 1], [:b, 2])
```

### Predicate matchers ### Predicate matchers


```ruby ```ruby
Expand Down
1 change: 1 addition & 0 deletions features/.nav
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- satisfy.feature - satisfy.feature
- throw_symbol.feature - throw_symbol.feature
- types.feature - types.feature
- yield.feature
- custom_matchers: - custom_matchers:
- define_matcher.feature - define_matcher.feature
- define_diffable_matcher.feature - define_diffable_matcher.feature
Expand Down
146 changes: 146 additions & 0 deletions features/built_in_matchers/yield.feature
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,146 @@
Feature: yield matchers

There are four related matchers that allow you to specify whether
or not a method yields, how many times it yields, whether or not
it yields with arguments, and what those arguments are.

* `yield_control` matches if the method-under-test yields, regardless
of whether or not arguments are yielded.
* `yield_with_args` matches if the method-under-test yields with
arguments. If arguments are provided to this matcher, it will
only pass if the actual yielded arguments match the expected ones
using `===` or `==`.
* `yield_with_no_args` matches if the method-under-test yields with
no arguments.
* `yield_successive_args` is designed for iterators, and will match
if the method-under-test yields the same number of times as arguments
passed to this matcher, and all actual yielded arguments match the
expected ones using `===` or `==`.

Note: your expect block _must_ accept an argument that is then passed on to
the method-under-test as a block. This acts as a "probe" that allows the matcher
to detect whether or not your method yields, and, if so, how many times and what
the yielded arguments are.

Background:
Given a file named "my_class.rb" with:
"""
class MyClass
def self.yield_once_with(*args)
yield *args
end

def self.raw_yield
yield
end

def self.dont_yield
end
end
"""

Scenario: yield_control matcher
Given a file named "yield_control_spec.rb" with:
"""
require './my_class'

describe "yield_control matcher" do
specify { expect { |b| MyClass.yield_once_with(1, &b) }.to yield_control }
specify { expect { |b| MyClass.dont_yield(&b) }.not_to yield_control }

# deliberate failures
specify { expect { |b| MyClass.yield_once_with(1, &b) }.not_to yield_control }
specify { expect { |b| MyClass.dont_yield(&b) }.to yield_control }
end
"""
When I run `rspec yield_control_spec.rb`
Then the output should contain all of these:
| 4 examples, 2 failures |
| expected given block to yield control |
| expected given block not to yield control |

Scenario: yield_with_args matcher
Given a file named "yield_with_args_spec.rb" with:
"""
require './my_class'

describe "yield_with_args matcher" do
specify { expect { |b| MyClass.yield_once_with("foo", &b) }.to yield_with_args }
specify { expect { |b| MyClass.yield_once_with("foo", &b) }.to yield_with_args("foo") }
specify { expect { |b| MyClass.yield_once_with("foo", &b) }.to yield_with_args(String) }
specify { expect { |b| MyClass.yield_once_with("foo", &b) }.to yield_with_args(/oo/) }

specify { expect { |b| MyClass.yield_once_with("foo", "bar", &b) }.to yield_with_args("foo", "bar") }
specify { expect { |b| MyClass.yield_once_with("foo", "bar", &b) }.to yield_with_args(String, String) }
specify { expect { |b| MyClass.yield_once_with("foo", "bar", &b) }.to yield_with_args(/fo/, /ar/) }

specify { expect { |b| MyClass.yield_once_with("foo", "bar", &b) }.not_to yield_with_args(17, "baz") }

# deliberate failures
specify { expect { |b| MyClass.yield_once_with("foo", &b) }.not_to yield_with_args }
specify { expect { |b| MyClass.yield_once_with("foo", &b) }.not_to yield_with_args("foo") }
specify { expect { |b| MyClass.yield_once_with("foo", &b) }.not_to yield_with_args(String) }
specify { expect { |b| MyClass.yield_once_with("foo", &b) }.not_to yield_with_args(/oo/) }
specify { expect { |b| MyClass.yield_once_with("foo", "bar", &b) }.not_to yield_with_args("foo", "bar") }
specify { expect { |b| MyClass.yield_once_with("foo", "bar", &b) }.to yield_with_args(17, "baz") }
end
"""
When I run `rspec yield_with_args_spec.rb`
Then the output should contain all of these:
| 14 examples, 6 failures |
| expected given block not to yield with arguments, but did |
| expected given block not to yield with arguments, but yielded with expected arguments |
| expected given block to yield with arguments, but yielded with unexpected arguments |

Scenario: yield_with_no_args matcher
Given a file named "yield_with_no_args_spec.rb" with:
"""
require './my_class'

describe "yield_with_no_args matcher" do
specify { expect { |b| MyClass.raw_yield(&b) }.to yield_with_no_args }
specify { expect { |b| MyClass.dont_yield(&b) }.not_to yield_with_no_args }
specify { expect { |b| MyClass.yield_once_with("a", &b) }.not_to yield_with_no_args }

# deliberate failures
specify { expect { |b| MyClass.raw_yield(&b) }.not_to yield_with_no_args }
specify { expect { |b| MyClass.dont_yield(&b) }.to yield_with_no_args }
specify { expect { |b| MyClass.yield_once_with("a", &b) }.to yield_with_no_args }
end
"""
When I run `rspec yield_with_no_args_spec.rb`
Then the output should contain all of these:
| 6 examples, 3 failures |
| expected given block not to yield with no arguments, but did |
| expected given block to yield with no arguments, but did not yield |
| expected given block to yield with no arguments, but yielded with arguments: ["a"] |

Scenario: yield_successive_args matcher
Given a file named "yield_successive_args_spec.rb" with:
"""
def array
[1, 2, 3]
end

def array_of_tuples
[[:a, :b], [:c, :d]]
end

describe "yield_successive_args matcher" do
specify { expect { |b| array.each(&b) }.to yield_successive_args(1, 2, 3) }
specify { expect { |b| array_of_tuples.each(&b) }.to yield_successive_args([:a, :b], [:c, :d]) }
specify { expect { |b| array.each(&b) }.to yield_successive_args(Fixnum, Fixnum, Fixnum) }
specify { expect { |b| array.each(&b) }.not_to yield_successive_args(1, 2) }

# deliberate failures
specify { expect { |b| array.each(&b) }.not_to yield_successive_args(1, 2, 3) }
specify { expect { |b| array_of_tuples.each(&b) }.not_to yield_successive_args([:a, :b], [:c, :d]) }
specify { expect { |b| array.each(&b) }.not_to yield_successive_args(Fixnum, Fixnum, Fixnum) }
specify { expect { |b| array.each(&b) }.to yield_successive_args(1, 2) }
end
"""
When I run `rspec yield_successive_args_spec.rb`
Then the output should contain all of these:
| 8 examples, 4 failures |
| expected given block not to yield successively with arguments, but yielded with expected arguments |
| expected given block to yield successively with arguments, but yielded with unexpected arguments |
82 changes: 82 additions & 0 deletions lib/rspec/matchers.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -554,6 +554,88 @@ def throw_symbol(expected_symbol=nil, expected_arg=nil)
BuiltIn::ThrowSymbol.new(expected_symbol, expected_arg) BuiltIn::ThrowSymbol.new(expected_symbol, expected_arg)
end end


# Passes if the method called in the expect block yields, regardless
# of whether or not arguments are yielded.
#
# @example
#
# expect { |b| 5.tap(&b) }.to yield_control
# expect { |b| "a".to_sym(&b) }.not_to yield_control
#
# @note Your expect block must accept a parameter and pass it on to
# the method-under-test as a block.
# @note This matcher is not designed for use with methods that yield
# multiple times.
def yield_control
BuiltIn::YieldControl.new
end

# Passes if the method called in the expect block yields with
# no arguments. Fails if it does not yield, or yields with arguments.
#
# @example
#
# expect { |b| User.transaction(&b) }.to yield_with_no_args
# expect { |b| 5.tap(&b) }.not_to yield_with_no_args # because it yields with `5`
# expect { |b| "a".to_sym(&b) }.not_to yield_with_no_args # because it does not yield
#
# @note Your expect block must accept a parameter and pass it on to
# the method-under-test as a block.
# @note This matcher is not designed for use with methods that yield
# multiple times.
def yield_with_no_args
BuiltIn::YieldWithNoArgs.new
end

# Given no arguments, matches if the method called in the expect
# block yields with arguments (regardless of what they are or how
# many there are).
#
# Given arguments, matches if the method called in the expect block
# yields with arguments that match the given arguments.
#
# Argument matching is done using `===` (the case match operator)
# and `==`. If the expected and actual arguments match with either
# operator, the matcher will pass.
#
# @example
#
# expect { |b| 5.tap(&b) }.to yield_with_args # because #tap yields an arg
# expect { |b| 5.tap(&b) }.to yield_with_args(5) # because 5 == 5
# expect { |b| 5.tap(&b) }.to yield_with_args(Fixnum) # because Fixnum === 5
# expect { |b| File.open("f.txt", &b) }.to yield_with_args(/txt/) # because /txt/ === "f.txt"
#
# expect { |b| User.transaction(&b) }.not_to yield_with_args # because it yields no args
# expect { |b| 5.tap(&b) }.not_to yield_with_args(1, 2, 3)
#
# @note Your expect block must accept a parameter and pass it on to
# the method-under-test as a block.
# @note This matcher is not designed for use with methods that yield
# multiple times.
def yield_with_args(*args)
BuiltIn::YieldWithArgs.new(*args)
end

# Designed for use with methods that repeatedly yield (such as
# iterators). Passes if the method called in the expect block yields
# multiple times with arguments matching those given.
#
# Argument matching is done using `===` (the case match operator)
# and `==`. If the expected and actual arguments match with either
# operator, the matcher will pass.
#
# @example
#
# expect { |b| [1, 2, 3].each(&b) }.to yield_successive_args(1, 2, 3)
# expect { |b| { :a => 1, :b => 2 }.each(&b) }.to yield_successive_args([:a, 1], [:b, 2])
# expect { |b| [1, 2, 3].each(&b) }.not_to yield_successive_args(1, 2)
#
# @note Your expect block must accept a parameter and pass it on to
# the method-under-test as a block.
def yield_successive_args(*args)
BuiltIn::YieldSuccessiveArgs.new(*args)
end

# Passes if actual contains all of the expected regardless of order. # Passes if actual contains all of the expected regardless of order.
# This works for collections. Pass in multiple args and it will only # This works for collections. Pass in multiple args and it will only
# pass if all args are found in collection. # pass if all args are found in collection.
Expand Down
4 changes: 4 additions & 0 deletions lib/rspec/matchers/built_in.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ module BuiltIn
autoload :RespondTo, 'rspec/matchers/built_in/respond_to' autoload :RespondTo, 'rspec/matchers/built_in/respond_to'
autoload :Satisfy, 'rspec/matchers/built_in/satisfy' autoload :Satisfy, 'rspec/matchers/built_in/satisfy'
autoload :ThrowSymbol, 'rspec/matchers/built_in/throw_symbol' autoload :ThrowSymbol, 'rspec/matchers/built_in/throw_symbol'
autoload :YieldControl, 'rspec/matchers/built_in/yield'
autoload :YieldWithArgs, 'rspec/matchers/built_in/yield'
autoload :YieldWithNoArgs, 'rspec/matchers/built_in/yield'
autoload :YieldSuccessiveArgs, 'rspec/matchers/built_in/yield'
end end
end end
end end
Expand Down
Loading