Yield matchers #129

Merged
merged 17 commits into from Apr 19, 2012
+844 −0
Split
View
@@ -112,6 +112,21 @@ expect { ... }.to throw_symbol(:symbol)
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
```ruby
View
@@ -19,6 +19,7 @@
- satisfy.feature
- throw_symbol.feature
- types.feature
+ - yield.feature
- custom_matchers:
- define_matcher.feature
- define_diffable_matcher.feature
@@ -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 |
View
@@ -554,6 +554,88 @@ def throw_symbol(expected_symbol=nil, expected_arg=nil)
BuiltIn::ThrowSymbol.new(expected_symbol, expected_arg)
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.
# This works for collections. Pass in multiple args and it will only
# pass if all args are found in collection.
@@ -26,6 +26,10 @@ module BuiltIn
autoload :RespondTo, 'rspec/matchers/built_in/respond_to'
autoload :Satisfy, 'rspec/matchers/built_in/satisfy'
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
Oops, something went wrong.