Skip to content

Commit

Permalink
more consistent setting of multiple expectations
Browse files Browse the repository at this point in the history
When calling expects/stubs with a hash (method_names_vs_return_values),
only the last expectation would get returned. Any further 'expectation
setting' methods chained to that call would, therefore, get called only
on the last expectation. This seems arbitrary, and neither evident nor
intuitive. We now 'extract' the expectation setting methods into a
_virtual_ interface, 'implemented' by both Expectation and
ExpectationSetting, and return ExpectationSetting instead of Expectation
from the expects/stubs methods. This allows us to pass any further
expectation setting method calls on to _each_ of the multiple
expectations, rather than just the _last_ (or some other arbitrary)
single expectation.
  • Loading branch information
nitishr committed Feb 19, 2020
1 parent 2840411 commit 4214427
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 25 deletions.
26 changes: 26 additions & 0 deletions lib/mocha/expectation_setting.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module Mocha
class ExpectationSetting
EXPECTATION_SETTING_METHODS = %w[
times twice once never at_least at_least_once at_most at_most_once
with yields multiple_yields returns raises throws then when in_sequence
].map(&:to_sym).freeze

attr_reader :expectations

def initialize(expectations)
@expectations = expectations
end

def respond_to_missing?(symbol, _include_all = false)
EXPECTATION_SETTING_METHODS.include?(symbol) || super
end

def method_missing(symbol, *arguments, &block)
if EXPECTATION_SETTING_METHODS.include?(symbol)
ExpectationSetting.new(@expectations.map { |e| e.send(symbol, *arguments, &block) })
else
super
end
end
end
end
10 changes: 5 additions & 5 deletions lib/mocha/mock.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'mocha/singleton_class'
require 'mocha/expectation'
require 'mocha/expectation_list'
require 'mocha/expectation_setting'
require 'mocha/invocation'
require 'mocha/names'
require 'mocha/receivers'
Expand Down Expand Up @@ -137,7 +138,7 @@ def expects(method_name_or_hash, backtrace = nil)
# object.stubs(:stubbed_method_one).returns(:result_one)
# object.stubs(:stubbed_method_two).returns(:result_two)
def stubs(method_name_or_hash, backtrace = nil)
anticipates(method_name_or_hash, backtrace) { |expectation| expectation.at_least(0) }
anticipates(method_name_or_hash, backtrace).at_least(0)
end

# Removes the specified stubbed methods (added by calls to {#expects} or {#stubs}) and all expectations associated with them.
Expand Down Expand Up @@ -356,17 +357,16 @@ def any_expectations?
end

# @private
def anticipates(method_name_or_hash, backtrace = nil, object = Mock.new(@mockery), &block)
Array(method_name_or_hash).map do |*args|
def anticipates(method_name_or_hash, backtrace = nil, object = Mock.new(@mockery))
ExpectationSetting.new(Array(method_name_or_hash).map do |*args|
args = args.flatten
method_name = args.shift
Mockery.instance.stub_method(object, method_name) unless object.is_a?(Mock)
ensure_method_not_already_defined(method_name)
expectation = Expectation.new(self, method_name, backtrace)
expectation.returns(args.shift) unless args.empty?
yield expectation if block
@expectations.add(expectation)
end.last
end)
end
end
end
6 changes: 3 additions & 3 deletions lib/mocha/object_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def expects(expected_methods_vs_return_values)
#
# @see Mock#stubs
def stubs(stubbed_methods_vs_return_values)
anticipates(stubbed_methods_vs_return_values) { |expectation| expectation.at_least(0) }
anticipates(stubbed_methods_vs_return_values).at_least(0)
end

# Removes the specified stubbed methods (added by calls to {#expects} or {#stubs}) and all expectations associated with them.
Expand Down Expand Up @@ -143,11 +143,11 @@ def unstub(*method_names)

private

def anticipates(expected_methods_vs_return_values, &block)
def anticipates(expected_methods_vs_return_values)
if frozen?
raise StubbingError.new("can't stub method on frozen object: #{mocha_inspect}", caller)
end
mocha.anticipates(expected_methods_vs_return_values, caller, self, &block)
mocha.anticipates(expected_methods_vs_return_values, caller, self)
end
end
end
27 changes: 27 additions & 0 deletions test/acceptance/expectations_on_multiple_methods_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,31 @@ def my_instance_method_2
end
assert_passed(test_result)
end

def test_should_configure_expectations_for_multiple_methods
instance = Class.new do
def my_instance_method_1
:original_return_value_1
end

def my_instance_method_2
:original_return_value_2
end
end.new
test_result = run_as_test do
instance.stubs(
:my_instance_method_1 => :new_return_value_1,
:my_instance_method_2 => :new_return_value_2
).at_least(2)
assert_equal :new_return_value_1, instance.my_instance_method_1
assert_equal :new_return_value_2, instance.my_instance_method_2
end
assert_failed(test_result)
assert_equal [
'not all expectations were satisfied',
'unsatisfied expectations:',
"- expected at least twice, invoked once: #{instance.mocha_inspect}.my_instance_method_2(any_parameters)",
"- expected at least twice, invoked once: #{instance.mocha_inspect}.my_instance_method_1(any_parameters)"
], test_result.failure_message_lines
end
end
20 changes: 3 additions & 17 deletions test/unit/mock_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def test_should_build_and_store_expectations
mock = build_mock
expectation = mock.expects(:method1)
assert_not_nil expectation
assert_equal [expectation], mock.__expectations__.to_a
assert_equal expectation.expectations, mock.__expectations__.to_a
end

def test_should_not_stub_everything_by_default
Expand Down Expand Up @@ -86,28 +86,14 @@ def test_should_create_and_add_expectations
mock = build_mock
expectation1 = mock.expects(:method1)
expectation2 = mock.expects(:method2)
assert_equal [expectation1, expectation2].to_set, mock.__expectations__.to_set
end

def test_should_pass_backtrace_into_expectation
mock = build_mock
backtrace = Object.new
expectation = mock.expects(:method1, backtrace)
assert_equal backtrace, expectation.backtrace
end

def test_should_pass_backtrace_into_stub
mock = build_mock
backtrace = Object.new
stub = mock.stubs(:method1, backtrace)
assert_equal backtrace, stub.backtrace
assert_equal (expectation1.expectations + expectation2.expectations).to_set, mock.__expectations__.to_set
end

def test_should_create_and_add_stubs
mock = build_mock
stub1 = mock.stubs(:method1)
stub2 = mock.stubs(:method2)
assert_equal [stub1, stub2].to_set, mock.__expectations__.to_set
assert_equal (stub1.expectations + stub2.expectations).to_set, mock.__expectations__.to_set
end

def test_should_invoke_expectation_and_return_result
Expand Down

0 comments on commit 4214427

Please sign in to comment.