class MiniSpec::Mocks::Validations
Public Class Methods
# File lib/minispec/mocks/validations.rb, line 6 def initialize base, object, context, *expected_messages expected_messages.empty? && raise(ArgumentError, 'Wrong number of arguments (3 for 4+)') expected_messages.all? {|m| m.is_a?(Symbol)} || raise(ArgumentError, 'Only symbols accepted') @base, @object, @context, @failed = base, object, context, false @expected_messages = expected_messages.freeze @messages = expected_and_received.freeze validate_received_messages! end
Public Instance Methods
expect received message(s) to raise a exception.
if no args given any raised exception accepted. if a class given it checks whether raised exception is of given type. if a string or regexp given it checks whether raised message matches it.
@example expect ‘a` to raise something
expect(obj).to_receive(:a).and_raise
@example expect ‘a` to raise ArgumentError
expect(obj).to_receive(:a).and_raise(ArgumentError)
@example raised exception should be of ArgumentError type and match /something/
expect(obj).to_receive(:a).and_raise([ArgumentError, /something/])
@example expect ‘a` to raise ArgumentError and `b` to raise RuntimeError
expect(obj).to_receive(:a, :b).and_raise(ArgumentError, RuntimeError)
@example expect ‘a` to raise ArgumentError matching /something/ and `b` to raise RuntimeError
expect(obj).to_receive(:a, :b).and_raise([ArgumentError, /something/], RuntimeError)
# File lib/minispec/mocks/validations/raise.rb, line 24 def and_raise *expected, &block return self if @failed # `and_raise` can be called without arguments expected.empty? || assert_given_arguments_match_received_messages(*expected, &block) received = raised_exceptions if block return @base.instance_exec(*received.values, &block) || exception_error!(@expected_messages, block, received) end expected = single_message_expected? ? {@expected_messages[0] => expected} : zipper(@expected_messages, expected) context = @context.merge(negation: nil, right_proc: nil) # do NOT alter @context received.each_pair do |msg,calls| # each message should raise as expected at least once calls.any? {|c| exception_raised?(c, context, *expected[msg]) == true} || exception_error!(msg, expected[msg], msg => calls) end self end
extending expectation by expecting a specific returned value
@example
expect(obj).to_receive(:a).and_return(1) # for this to pass `obj.a` should return 1
@example
expect(obj).to_receive(:a, :b).and_return(1, 2) # for this to pass `obj.a` should return 1 and `obj.b` should return 2
@example using a block to validate returned value
expect(obj).to_receive(:a).and_return {|v| v == 1} # for this to pass `obj.a` should return 1
# File lib/minispec/mocks/validations/return.rb, line 17 def and_return *expected, &block return self if @failed assert_given_arguments_match_received_messages(*expected, &block) received = returned_values if block return @base.instance_exec(*received.values, &block) || returned_value_error!(@expected_messages, block, received) end expected = zipper(@expected_messages, expected) received.each_pair do |msg,values| # each message should return expected value at least once values.any? {|v| validate_returned_value(expected[msg], v)} || returned_value_error!(msg, expected[msg], msg => values) end self end
checks whether received message throws expected symbol
@note you can match against thrown symbol but not against value.
this is a WONTFIX limitation. though it is doable this would introduce a new layer of unproven complexity.
@example
expect(obj).to_receive(:a).and_throw(:something)
@example
expect(obj).to_receive(:a, :b).and_throw(:A, :B) # for this to pass `obj.a` should throw :A and `obj.b` :B
# File lib/minispec/mocks/validations/throw.rb, line 15 def and_throw *expected, &block return self if @failed expected.all? {|x| x.is_a?(Symbol)} || raise(ArgumentError, '`and_throw` accepts only symbols') # `and_throw` can be called without arguments expected.empty? || assert_given_arguments_match_received_messages(*expected, &block) received = thrown_symbols if block return @base.instance_exec(*received.values, &block) || throw_error!(@expected_messages, block, received) end expected = zipper(@expected_messages, expected) received.each_pair do |msg,calls| # each message should throw expected symbol at least once. # if no specific symbol expected, check whether any symbol thrown. calls.any? {|s| expected[msg] ? s == expected[msg] : s.is_a?(Symbol)} || throw_error!(msg, expected[msg], msg => calls) end self end
extending expectation by expecting received message to yield
@example
class Apple def color yield end def taste end end describe Apple do testing :color do apple = Apple.new expect(apple).to_receive(:color).and_yield # => will pass expect(apple).to_receive(:taste).and_yield # => will fail end end
@example
class Apple def color yield 1, 2 end end describe Apple do testing :color do apple = Apple.new expect(apple).to_receive(:color).and_yield(1, 2) # => will pass expect(apple).to_receive(:taste).and_yield(:something) # => will fail end end
# File lib/minispec/mocks/validations/yield.rb, line 43 def and_yield *expected, &block return self if @failed # `and_yield` can be called without arguments expected.empty? || assert_given_arguments_match_received_messages(*expected, &block) received = yielded_values if block return @base.instance_exec(*received.values, &block) || yield_error!(@expected_messages, block, received) end single_message_expected? ? validate_yields(expected, received) : validate_yields_list(expected, received) self end
assure expected message(s) was received a specific amount of times
@example expect ‘a` to be received exactly 2 times
expect(obj).to_receive(:a).count(2)
@example expect ‘a` to be received 2 or more times
expect(obj).to_receive(:a).count {|a| a >= 2}
@example expect ‘a` and `b` to be received 2 times each
expect(obj).to_receive(:a, :b).count(2)
@example expect ‘a` to be received 2 times and `b` 3 times
expect(obj).to_receive(:a, :b).count(2, 3)
@example expect both ‘a` and `b` to be received more than 2 times
expect(obj).to_receive(:a, :b).count {|a,b| a > 2 && b > 2}
# File lib/minispec/mocks/validations/amount.rb, line 20 def count *expected, &block return self if @failed assert_given_arguments_match_received_messages(*expected, &block) received = received_amounts if block return @base.instance_exec(*received.values, &block) || amount_error!(@expected_messages, block, received) end expected = zipper(@expected_messages, expected) received.each_pair do |message,amount| # each message should be received expected amount of times amount == expected[message] || amount_error!(message, expected[message], amount) end self end
# File lib/minispec/mocks/validations/amount.rb, line 40 def once; count(1); end
checks whether expected messages was received in a specific order
@note this method will work only when multiple messages expected.
that's it, unlike RSpec, it wont work like this: `expect(obj).to_receive(:a).ordered` `expect(obj).to_receive(:b).ordered` instead it will work like this: `expect(obj).to_receive(:a, :b).ordered`
@example
expect(obj).to_receive(:a, :b, :c).ordered
@example expect for same sequence N times
expect(obj).to_receive(:a, :b).ordered(2) # for this to pass `obj.a` and `obj.b` should be both called twice in same order
# File lib/minispec/mocks/validations/order.rb, line 20 def ordered n = 1, &block block && raise(ArgumentError, '#ordered does not accept a block') n.is_a?(Integer) || raise(ArgumentError, '#ordered expects a single Integer argument') single_message_expected? && raise(ArgumentError, '#ordered works only with multiple messages') received_in_expected_order?(n) end
# File lib/minispec/mocks/validations/amount.rb, line 41 def twice; count(2); end
validates received arguments against expected ones
@example
expect(obj).to_receive(:a).with(1) obj.a(1)
@example
expect(obj).to_receive(:a).with(1, 2) obj.a(1, 2)
@example
expect(obj).to_receive(:a).with(1, [:a, :b, :c]) obj.a(1, [:a, :b, :c])
@example
expect(obj).to_receive(:a).with {|x| x[0] == [1, 2, 3] && x[1] == [:x, [:y], 'z']} obj.a(1, 2, 3) obj.a(:x, [:y], 'z')
@example
expect(obj).to_receive(:a, :b, :c).with(1) obj.a(1) obj.b(1) obj.c(1)
@example
expect(obj).to_receive(:a, :b, :c).with(1, 2, 3) obj.a(1) obj.b(2) obj.c(3)
@example
expect(obj).to_receive(:a, :b, :c).with([1, 2, 3]) obj.a(1, 2, 3) obj.b(1, 2, 3) obj.c(1, 2, 3)
@example
expect(obj).to_receive(:a, :b, :c).with([1, 2], [:x, :y], :z) obj.a(1, 2) obj.b(:x, :y) obj.c(:z)
@example
expect(obj).to_receive(:a, :b, :c).with([[1, 2]], [[:x, :y]], [:z]) obj.a([1, 2]) obj.b([:x, :y]) obj.c([:z])
@example
expect(obj).to_receive(:a, :b, :c).with do |a,b,c| a == [[1, 2]] && b == [[:x, :y]] && c == [:z] end obj.a(1, 2) obj.b(:x, :y) obj.c(:z)
@example
expect(obj).to_receive(:a, :b, :c).with do |a,b,c| a == [[1, 2], [3, 4]] && b == [[:x, :y], [2]] && c == [[:z], [[:a, :b], :c]] end obj.a(1, 2) obj.a(3, 4) obj.b(:x, :y) obj.b(2) obj.c(:z) obj.c([:a, :b], :c)
# File lib/minispec/mocks/validations/arguments.rb, line 75 def with *expected, &block return self if @failed assert_given_arguments_match_received_messages(*expected, &block) received = received_arguments if block return @base.instance_exec(*received.values, &block) || arguments_error!(@expected_messages, block, received) end single_message_expected? ? validate_arguments(expected, received) : validate_arguments_list(expected, received) self end
# File lib/minispec/mocks/validations/caller.rb, line 3 def with_caller *expected, &block return self if @failed assert_given_arguments_match_received_messages(*expected, &block) received = received_callers if block return @base.instance_exec(*received.values, &block) || caller_error!(@expected_messages, block) end expected = zipper(@expected_messages, expected) received.each_pair do |msg,callers| # each message should be called from expected caller at least once callers.any? {|line| caller_match?(line, expected[msg])} || caller_error!(msg, expected[msg]) end self end
# File lib/minispec/mocks/validations/arguments.rb, line 91 def without_arguments return self if @failed received_arguments.each_pair do |msg,args| # each message should be called without arguments at least once args.any?(&:empty?) || arguments_error!(msg, [], msg => args) end self end
make sure received message(s) does not raise any exception
@example
expect(obj).to_receive(:a).without_raise
# File lib/minispec/mocks/validations/raise.rb, line 54 def without_raise return self if @failed raised_exceptions.each_pair do |msg,calls| calls.any? {|r| r.is_a?(Exception)} && unexpected_exception_error!(msg, calls) end self end
assure received message does not throw a symbol
@example
expect(obj).to_receive(:a).without_throw
# File lib/minispec/mocks/validations/throw.rb, line 44 def without_throw return self if @failed thrown_symbols.each_pair do |msg,calls| calls.any? {|x| x.is_a?(Symbol)} && unexpected_throw_error!(msg, calls) end self end
make sure received message wont yield
@example
expect(:obj).to_receive(:a).without_yield
# File lib/minispec/mocks/validations/yield.rb, line 67 def without_yield return self if @failed yielded_values.each_pair do |msg,calls| next if calls.all?(&:nil?) unexpected_yield_error!(msg, calls) end self end
Private Instance Methods
# File lib/minispec/mocks/validations/amount.rb, line 53 def amount_error! messages, expected, received fail_with("%s received %s message(s) wrong amount of times.\nExpected: %s\nActual: %s" % [ pp(@object), pp(messages), expected.is_a?(Proc) ? ('to be validated at %s' % pp(source(expected))) : Array(expected).map {|x| pp(x)}*', ', pp(received) ]) end
# File lib/minispec/mocks/validations/arguments.rb, line 153 def arguments_error! message, expected, received fail_with("%s received %s message(s) with unexpected arguments.\nExpected: %s\nActual: %s" % [ pp(@object), pp(message), stringify_expected_arguments(expected), stringify_received_arguments(received) ]) end
checks whether correct number of arguments given. in any situation, at least one argument required. if multiple messages expected, number of arguments should be equal to one or to the number of expected messages.
# File lib/minispec/mocks/validations.rb, line 50 def assert_given_arguments_match_received_messages *args, &block if block args.empty? || raise(ArgumentError, 'Both arguments and block given. Please use either one.') return true # if block given, no arguments accepted, so nothing to validate end # single argument acceptable for any number of expected messages return if args.size == 1 # when a single message expected, any number of arguments accepted return if @expected_messages.size == 1 # on multiple messages, number of arguments should match number of expected messages return if args.size == @expected_messages.size raise(ArgumentError, 'wrong number of arguments (%i for 1..%i)' % [ args.size, @expected_messages.size ], caller[1..-1]) end
# File lib/minispec/mocks/validations/caller.rb, line 34 def caller_error! message, expected fail_with("%s received %s message(s) from wrong location.\nCaller does not %s" % [ pp(@object), pp(message), expected.is_a?(Proc) ? ('pass validation at %s' % pp(source(expected))) : ('match %s' % pp(expected)) ]) end
# File lib/minispec/mocks/validations/caller.rb, line 29 def caller_match? line, pattern regexp = pattern.is_a?(Regexp) ? pattern : Regexp.new(Regexp.escape(pattern)) line.any? {|l| l =~ regexp} end
# File lib/minispec/mocks/validations/raise.rb, line 78 def exception_error! message, expected, received fail_with("%s received %s message(s) but did not raise accordingly.\nExpected: %s\nActual: %s" % [ pp(@object), pp(message), stringify_expected_exception(expected), stringify_received_exception(received) ]) end
selecting only expected messages in the order they was received. @param expected_messages [Array] @return [Hash]
# File lib/minispec/mocks/validations.rb, line 19 def expected_and_received @base.__ms__mocks__instance_messages(@context[:left_object]).inject({}) do |map,msg| @expected_messages.include?(msg[:method]) && (map[msg[:method]] ||= []).push(msg) map end end
# File lib/minispec/mocks/validations.rb, line 71 def fail_with message return unless @failed = message @base.fail(message) self end
# File lib/minispec/mocks/validations.rb, line 34 def message_validation_error! message, negation = false fail_with('%sExpected %s to receive %s message' % [ negation ? 'NOT ' : '', pp(@object), pp(message), ]) end
returns an Array of all messages in the order they was received
# File lib/minispec/mocks/validations/order.rb, line 35 def messages_in_received_order @base.__ms__mocks__instance_messages(@context[:left_object]).map {|m| m[:method]} end
# File lib/minispec/mocks/validations/order.rb, line 39 def ordered_error! expected, received fail_with("Expected %s to receive %s sequence %s times.\nInstead it was received %s times." % [ pp(@object), pp(@expected_messages), expected, received ]) end
# File lib/minispec/mocks/validations/raise.rb, line 63 def raised_exceptions @expected_messages.inject({}) do |map,msg| map.merge(msg => @messages[msg] ? @messages[msg].map {|m| m[:raised]} : []) end end
returns a Hash of messages each with amount of times it was called. basically it does the same as ‘@messages.values.map(&:size)` except it returns the messages in the order they are expected.
# File lib/minispec/mocks/validations/amount.rb, line 47 def received_amounts @expected_messages.inject({}) do |map,msg| map.merge(msg => (@messages[msg] || []).size) end end
returns a Hash of received messages, each with a list of arguments it was called with.
@example
obj.a(:x) obj.a([:x]) obj.a(:y, [:z]) => { a: [ [:x], [[:x]], [:y, [:z]] ] }
# File lib/minispec/mocks/validations/arguments.rb, line 111 def received_arguments @expected_messages.inject({}) do |map,msg| map.merge(msg => @messages[msg] ? @messages[msg].map {|m| m[:arguments]} : []) end end
# File lib/minispec/mocks/validations/caller.rb, line 23 def received_callers @expected_messages.inject({}) do |map,msg| map.merge(msg => @messages[msg] ? @messages[msg].map {|m| m[:caller]} : []) end end
# File lib/minispec/mocks/validations/order.rb, line 28 def received_in_expected_order? n x = 0 messages_in_received_order.each_cons(@expected_messages.size) {|c| x += 1 if c == @expected_messages} x == n || ordered_error!(n, x) end
# File lib/minispec/mocks/validations/return.rb, line 51 def returned_value_error! message, expected, received fail_with("%s received %s message(s) and returned unexpected value(s).\nExpected: %s\nActual: %s" % [ pp(@object), pp(message), expected.is_a?(Proc) ? 'to pass validation at %s' % pp(source(expected)) : pp(expected), stringify_returned_values(received) ]) end
# File lib/minispec/mocks/validations/return.rb, line 38 def returned_values @expected_messages.inject({}) do |map,msg| map.merge(msg => @messages[msg] ? @messages[msg].map {|m| m[:returned]} : []) end end
# File lib/minispec/mocks/validations.rb, line 42 def single_message_expected? @expected_messages.size == 1 end
# File lib/minispec/mocks/validations/raise.rb, line 108 def stringify_exception exception [exception.class, exception.message].map(&method(:pp))*':' end
# File lib/minispec/mocks/validations/arguments.rb, line 132 def stringify_expected_arguments expected return 'to be validated at %s' % pp(source(expected)) if expected.is_a?(Proc) expected = Array(expected) return 'to be called without arguments' if expected.empty? expected.map {|a| pp(a)}*', ' end
# File lib/minispec/mocks/validations/raise.rb, line 87 def stringify_expected_exception expected return 'any exception to be raised' unless expected return 'raised exception to be validated at %s' % pp(source(expected)) if expected.is_a?(Proc) Array(expected).map(&method(:pp))*':' end
# File lib/minispec/mocks/validations/yield.rb, line 123 def stringify_expected_yields expected return 'yielded values to pass validation at %s' % pp(source(expected)) if expected.is_a?(Proc) return 'something to be yielded' if expected.empty? pp(expected) end
# File lib/minispec/mocks/validations/arguments.rb, line 139 def stringify_received_arguments received received.is_a?(Hash) || raise(ArgumentError, 'expected a Hash') received.map do |msg,args| '%s called %s' % [ pp(msg), args.map do |arr| arr.empty? ? 'without arguments' : 'with %s' % arr.map {|a| pp(a)}.join(', ') end*' then ' ] end*"\n " end
# File lib/minispec/mocks/validations/raise.rb, line 93 def stringify_received_exception received received.is_a?(Hash) || raise(ArgumentError, 'a Hash expected') received.map do |msg,calls| calls.each_with_index.map do |call,i| '%s call #%s raised %s' % [ pp(msg), i + 1, call.is_a?(Exception) ? stringify_exception(call) : 'nothing' ] end*"\n " end*"\n " end
# File lib/minispec/mocks/validations/yield.rb, line 129 def stringify_received_yields received received.is_a?(Hash) || raise(ArgumentError, 'a Hash expected') received.map do |msg,calls| calls.each_with_index.map do |call,i| '%s call #%s yielded %s' % [ pp(msg), i + 1, call ? pp(call) : 'nothing' ] end*"\n " end*"\n " end
# File lib/minispec/mocks/validations/return.rb, line 62 def stringify_returned_values returned returned.is_a?(Hash) || raise(ArgumentError, 'a Hash expected') returned.map do |msg,values| values.each_with_index.map do |value,i| '%s call #%s returned %s' % [ pp(msg), i + 1, pp(value) ] end*"\n " end*"\n " end
# File lib/minispec/mocks/validations/throw.rb, line 79 def stringify_thrown_symbols received received.is_a?(Hash) || raise(ArgumentError, 'a Hash expected') received.map do |msg,calls| calls.each_with_index.map do |call,i| '%s call #%s thrown %s' % [ pp(msg), i + 1, call.is_a?(Symbol) ? pp(call) : 'nothing' ] end*"\n " end*"\n " end
# File lib/minispec/mocks/validations/throw.rb, line 68 def throw_error! message, expected, received fail_with("%s received %s message(s) but did not throw accordingly.\nExpected: %s\nActual: %s" % [ pp(@object), pp(message), expected.is_a?(Proc) ? 'results to be validated at %s' % pp(source(expected)) : pp(expected), stringify_thrown_symbols(received) ]) end
# File lib/minispec/mocks/validations/throw.rb, line 53 def thrown_symbols @expected_messages.inject({}) do |map,msg| map.merge(msg => @messages[msg] ? @messages[msg].map {|m| extract_thrown_symbol(m[:raised])} : []) end end
# File lib/minispec/mocks/validations/raise.rb, line 69 def unexpected_exception_error! message, received fail_with("%s received %s message and raised an unexpected error.\nExpected: %s\nActual: %s" % [ pp(@object), pp(message), 'nothing to be raised', stringify_received_exception(message => received) ]) end
# File lib/minispec/mocks/validations/throw.rb, line 59 def unexpected_throw_error! message, received fail_with("%s received %s message(s) and thrown an unexpected symbol.\nExpected: %s\nActual: %s" % [ pp(@object), pp(message), 'nothing to be thrown', stringify_thrown_symbols(message => received) ]) end
# File lib/minispec/mocks/validations/yield.rb, line 105 def unexpected_yield_error! message, received fail_with("%s received %s message and unexpectedly yielded.\nExpected: %s\nActual: %s" % [ pp(@object), pp(message), 'nothing to be yielded', stringify_received_yields(message => received) ]) end
# File lib/minispec/mocks/validations/arguments.rb, line 117 def validate_arguments expected, received message = @expected_messages[0] arguments = received[message] return if arguments.any? {|x| x == expected} arguments_error!(message, expected, message => arguments) end
# File lib/minispec/mocks/validations/arguments.rb, line 124 def validate_arguments_list expected, received expected = zipper(@expected_messages, expected) received.each_pair do |msg,args| next if args.any? {|x| x == [expected[msg]]} arguments_error!(msg, expected[msg], msg => args) end end
# File lib/minispec/mocks/validations.rb, line 26 def validate_received_messages! @expected_messages.each do |m| @context[:negation] ? @messages.keys.include?(m) && message_validation_error!(m, true) : @messages.keys.include?(m) || message_validation_error!(m) end end
# File lib/minispec/mocks/validations/return.rb, line 44 def validate_returned_value expected, returned if expected.is_a?(Regexp) return returned.is_a?(Regexp) ? expected == returned : returned.to_s =~ expected end expected == returned end
# File lib/minispec/mocks/validations/yield.rb, line 83 def validate_yields expected, received message = @expected_messages[0] calls = received[message] return if validate_yields_calls(calls, expected) yield_error!(message, expected, message => calls) end
# File lib/minispec/mocks/validations/yield.rb, line 99 def validate_yields_calls calls, expected expected.nil? || expected.empty? ? calls.any? {|c| c.is_a?(Array)} : calls.any? {|c| c == expected} end
# File lib/minispec/mocks/validations/yield.rb, line 90 def validate_yields_list expected, received expected = zipper(@expected_messages, expected) received.each_pair do |msg,calls| expect = Array(expected[msg]).flatten(1) next if validate_yields_calls(calls, expect) yield_error!(msg, expect, msg => calls) end end
# File lib/minispec/mocks/validations/yield.rb, line 114 def yield_error! message, expected, received fail_with("%s received %s message(s) but did not yield accordingly.\nExpected: %s\nActual: %s" % [ pp(@object), pp(message), stringify_expected_yields(expected), stringify_received_yields(received) ]) end
# File lib/minispec/mocks/validations/yield.rb, line 77 def yielded_values @expected_messages.inject({}) do |map,msg| map.merge(msg => @messages[msg] ? @messages[msg].map {|m| m[:yielded]} : []) end end