#!/usr/bin/env ruby #--- # Copyright 2003, 2004, 2005 by Jim Weirich (jim@weirichhouse.org). # All rights reserved. # Permission is granted for use, copying, modification, distribution, # and distribution of modified versions of this work as long as the # above copyright notice is included. #+++ require 'test/unit' ###################################################################### # FlexMock is a flexible mock object suitable for using with Ruby's # Test::Unit unit test framework. FlexMock has a simple interface # that's easy to remember, and leaves the hard stuff to all those # other mock object implementations. # # Basic Usage: # # m = FlexMock.new("name") # m.mock_handle(:meth) { |args| assert_stuff } # # Simplified Usage: # # m = FlexMock.new("name") # m.should_receive(:upcase).with("stuff"). # returns("STUFF") # m.should_receive(:downcase).with(String). # returns { |s| s.downcase }.once # class FlexMock include Test::Unit::Assertions attr_reader :mock_name, :mock_groups attr_accessor :mock_current_order # Create a FlexMock object with the given name. The name is used in # error messages. def initialize(name="unknown") @mock_name = name @expectations = Hash.new @allocated_order = 0 @mock_current_order = 0 @mock_groups = {} end # Handle all messages denoted by +sym+ by calling the given block # and passing any parameters to the block. If we know exactly how # many calls are to be made to a particular method, we may check # that by passing in the number of expected calls as a second # paramter. def mock_handle(sym, expected_count=nil, &block) self.should_receive(sym).times(expected_count).returns(&block) end # Verify that each method that had an explicit expected count was # actually called that many times. def mock_verify mock_wrap do @expectations.each do |sym, handler| handler.mock_verify end end end # Allocation a new order number from the mock. def mock_allocate_order @auto_allocate = true @allocated_order += 1 end # Ignore all undefined (missing) method calls. def should_ignore_missing @ignore_missing = true end alias mock_ignore_missing should_ignore_missing # Handle missing methods by attempting to look up a handler. def method_missing(sym, *args, &block) mock_wrap do if handler = @expectations[sym] args << block if block_given? handler.call(*args) else super(sym, *args, &block) unless @ignore_missing end end end # Override the built-in respond_to? to include the mocked methods. def respond_to?(sym) super || (@expectations[sym] ? true : @ignore_missing) end # Override the built-in +method+ to include the mocked methods. def method(sym) @expectations[sym] || super rescue NameError => ex if @ignore_missing proc { } else raise ex end end # Declare that the mock object should receive a message with the # given name. An expectation object for the method name is returned # as the result of this method. Further expectation constraints can # be added by chaining to the result. # # See Expectation for a list of declarators that can be used. def should_receive(sym) @expectations[sym] ||= ExpectationDirector.new(sym) result = Expectation.new(self, sym) @expectations[sym] << result result end class << self include Test::Unit::Assertions # Class method to make sure that verify is called at the end of a # test. One mock object will be created for each name given to # the use method. The mocks will be passed to the block as # arguments. If no names are given, then a single anonymous mock # object will be created. # # At the end of the use block, each mock object will be verified # to make sure the proper number of calls have been made. # # Usage: # # FlexMock.use("name") do |mock| # Creates a mock named "name" # mock.should_receive(:meth). # returns(0).once # end # mock is verified here # def use(*names) names = ["unknown"] if names.empty? got_excecption = false mocks = names.collect { |n| new(n) } yield(*mocks) rescue Exception => ex got_exception = true raise ensure mocks.each do |mock| mock.mock_verify unless got_exception end end # Class method to format a method name and argument list as a nice # looking string. def format_args(sym, args) if args "#{sym}(#{args.collect { |a| a.inspect }.join(', ')})" else "#{sym}(*args)" end end # Check will assert the block returns true. If it doesn't, an # assertion failure is triggered with the given message. def check(msg, &block) assert_block(msg, &block) end end private # Wrap a block of code so the any assertion errors are wrapped so # that the mock name is added to the error message . def mock_wrap(&block) yield rescue Test::Unit::AssertionFailedError => ex raise Test::Unit::AssertionFailedError, "in mock '#{@mock_name}': #{ex.message}", ex.backtrace end #################################################################### # The expectation director is responsible for routing calls to the # correct expectations for a given argument list. # class ExpectationDirector # Create an ExpectationDirector for a mock object. def initialize(sym) @sym = sym @expectations = [] @expected_order = nil end # Invoke the expectations for a given set of arguments. def call(*args) exp = @expectations.find { |e| e.match_args(args) } || @expectations.find { |e| e.expected_args.nil? } FlexMock.check("no matching handler found for " + FlexMock.format_args(@sym, args)) { ! exp.nil? } exp.verify_call(*args) end # Same as call. def [](*args) call(*args) end # Append an expectation to this director. def <<(expectation) @expectations << expectation end # Do the post test verification for this directory. Check all the # expectations. def mock_verify @expectations.each do |exp| exp.mock_verify end end end #################################################################### # Match any object class AnyMatcher def ===(target) true end def inspect "ANY" end end #################################################################### # Match only things that are equal. class EqualMatcher def initialize(obj) @obj = obj end def ===(target) @obj == target end def inspect "==(#{@obj.inspect})" end end ANY = AnyMatcher.new #################################################################### # Include this module in your test class if you wish to use the +eq+ # and +any+ argument matching methods without a prefix. (Otherwise # use FlexMock.any and FlexMock.eq(obj). # module ArgumentTypes # Return an argument matcher that matches any argument. def any ANY end # Return an argument matcher that only matches things equal to # (==) the given object. def eq(obj) EqualMatcher.new(obj) end end #################################################################### # Base class for all the count validators. # class CountValidator include Test::Unit::Assertions def initialize(expectation, limit) @exp = expectation @limit = limit end end #################################################################### # Validator for exact call counts. # class ExactCountValidator < CountValidator # Validate that the method expectation was called exactly +n+ # times. def validate(n) assert_equal @limit, n, "method '#{@exp}' called incorrect number of times" end end #################################################################### # Validator for call counts greater than or equal to a limit. # class AtLeastCountValidator < CountValidator # Validate the method expectation was called no more than +n+ # times. def validate(n) assert n >= @limit, "Method '#{@exp}' should be called at least #{@limit} times,\n" + "only called #{n} times" end end #################################################################### # Validator for call counts less than or equal to a limit. # class AtMostCountValidator < CountValidator # Validate the method expectation was called at least +n+ times. def validate(n) assert n <= @limit, "Method '#{@exp}' should be called at most #{@limit} times,\n" + "only called #{n} times" end end #################################################################### # An Expectation is returned from each +should_receive+ message sent # to mock object. Each expectation records how a message matching # the message name (argument to +should_receive+) and the argument # list (given by +with+) should behave. Mock expectations can be # recorded by chaining the declaration methods defined in this # class. # # For example: # # mock.should_receive(:meth).with(args).and_returns(result) # class Expectation include Test::Unit::Assertions attr_reader :expected_args, :mock, :order_number # Create an expectation for a method named +sym+. def initialize(mock, sym) @mock = mock @sym = sym @expected_args = nil @count_validators = [] @count_validator_class = ExactCountValidator @actual_count = 0 @return_value = nil @return_block = lambda { @return_value } @order_number = nil end def to_s FlexMock.format_args(@sym, @expected_args) end # Verify the current call with the given arguments matches the # expectations recorded in this object. def verify_call(*args) validate_order @actual_count += 1 @return_block.call(*args) end # Validate that the order def validate_order return if @order_number.nil? FlexMock.check("method #{to_s} called out of order " + "(expected order #{@order_number}, was #{@mock.mock_current_order})") { @order_number >= @mock.mock_current_order } @mock.mock_current_order = @order_number end private :validate_order # Validate the correct number of calls have been made. Called by # the teardown process. def mock_verify @count_validators.each do |v| v.validate(@actual_count) end end # Does the argument list match this expectation's argument # specification. def match_args(args) return false if @expected_args.nil? return false if args.size != @expected_args.size (0...args.size).all? { |i| match_arg(@expected_args[i], args[i]) } end # Does the expected argument match the corresponding actual value. def match_arg(expected, actual) expected === actual || expected == actual || ( Regexp === expected && expected === actual.to_s ) end # Declare that the method should expect the given argument list. def with(*args) @expected_args = args self end # Declare that the method should be called with no arguments. def with_no_args with end # Declare that the method can be called with any number of # arguments of any type. def with_any_args @expected_args = nil self end # Declare that the method returns a particular value (when the # argument list is matched). If a block is given, it is evaluated # on each call and its value is returned. +and_return+ is an # alias for +returns+. # # For example: # # mock.should_receive(:f).returns(12) # returns 12 # # mock.should_receive(:f).with(String). # returns an # returns { |str| str.upcase } # upcased string # def returns(value=nil, &block) @return_block = block_given? ? block : lambda { value } self end alias :and_return :returns # :nodoc: # Declare that the method may be called any number of times. def zero_or_more_times at_least.never end # Declare that the method is called +limit+ times with the # declared argument list. This may be modified by the +at_least+ # and +at_most+ declarators. def times(limit) @count_validators << @count_validator_class.new(self, limit) unless limit.nil? @count_validator_class = ExactCountValidator self end # Declare that the method is never expected to be called with the # given argument list. This may be modified by the +at_least+ and # +at_most+ declarators. def never times(0) end # Declare that the method is expected to be called exactly once # with the given argument list. This may be modified by the # +at_least+ and +at_most+ declarators. def once times(1) end # Declare that the method is expected to be called exactly twice # with the given argument list. This may be modified by the # +at_least+ and +at_most+ declarators. def twice times(2) end # Modifies the next call count declarator (+times+, +never+, # +once+ or +twice+) so that the declarator means the method is # called at least that many times. # # E.g. method f must be called at least twice: # # mock.should_receive(:f).at_least.twice # def at_least @count_validator_class = AtLeastCountValidator self end # Modifies the next call count declarator (+times+, +never+, # +once+ or +twice+) so that the declarator means the method is # called at most that many times. # # E.g. method f must be called no more than twice # # mock.should_receive(:f).at_most.twice # def at_most @count_validator_class = AtMostCountValidator self end # Declare that the given method must be called in order. All # ordered method calls must be received in the order specified by # the ordering of the +should_receive+ messages. Receiving a # methods out of the specified order will cause a test failure. # # If the user needs more fine control over ordering # (e.g. specifying that a group of messages may be received in any # order as long as they all come after another group of messages), # a _group_ _name_ may be specified in the +ordered+ calls. All # messages within the same group may be recieved in any order. # # For example, in the following, messages +flip+ and +flop+ may be # received in any order (because they are in the same group), but # must occur strictly after +start+ but before +end+. The message # +any_time+ may be received at any time because it is not # ordered. # # m = FlexMock.new # m.should_receive(:any_time) # m.should_receive(:start).ordered # m.should_receive(:flip).ordered(:flip_flop_group) # m.should_receive(:flop).ordered(:flip_flop_group) # m.should_receive(:end).ordered # def ordered(group_name=nil) if group_name.nil? @order_number = @mock.mock_allocate_order elsif (num = @mock.mock_groups[group_name]) @order_number = num else @order_number = @mock.mock_allocate_order @mock.mock_groups[group_name] = @order_number end self end end extend ArgumentTypes end