#!/usr/bin/env ruby #--- # Copyright 2003, 2004, 2005, 2006, 2007 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 'flexmock/noop' require 'flexmock/argument_types' class FlexMock # ###################################################################### # Mock container methods # # Include this module in to get integration with FlexMock. When this module # is included, mocks may be created with a simple call to the +flexmock+ # method. Mocks created with via the method call will automatically be # verified in the teardown of the test case. # module MockContainer include Ordering # Do the flexmock specific teardown stuff. If you need finer control, # you can use either +flexmock_verify+ or +flexmock_close+. def flexmock_teardown flexmock_verify if passed? ensure flexmock_close end # Perform verification on all mocks in the container. def flexmock_verify @flexmock_created_mocks ||= [] @flexmock_created_mocks.each do |m| m.flexmock_verify end end # Close all the mock objects in the container. Closing a mock object # restores any original behavior that was displaced by the mock. def flexmock_close @flexmock_created_mocks ||= [] @flexmock_created_mocks.each do |m| m.flexmock_teardown end @flexmock_created_mocks = [] end # Create a mocking object in the FlexMock framework. The +flexmock+ # method has a number of options available, depending on just what kind of # mocking object your require. Mocks created via +flexmock+ will be # automatically verify during the teardown phase of your test framework. # # :call-seq: # flexmock() { |mock| ... } # flexmock(name) { |mock| ... } # flexmock(expect_hash) { |mock| ... } # flexmock(name, expect_hash) { |mock| ... } # flexmock(real_object) { |mock| ... } # flexmock(real_object, name) { |mock| ... } # flexmock(real_object, name, expect_hash) { |mock| ... } # flexmock(:base, string, name, expect_hash) { |mock| ... } # # Note: A plain flexmock() call without a block will return the # mock object (the object that interprets should_receive and its # brethern). A flexmock() call that _includes_ a block will return the # domain objects (the object that will interpret domain messages) since # the mock will be passed to the block for configuration. With regular # mocks, this distinction is unimportant because the mock object and the # domain object are the same object. However, with partial mocks, the # mock object is separation from the domain object. Keep that distinciton # in mind. # # name :: # Name of the mock object. If no name is given, "unknown" is used for # full mocks and "flexmock(real_object)" is used for partial # mocks. # # expect_hash :: # Hash table of method names and values. Each method/value pair is # used to setup a simple expectation so that if the mock object # receives a message matching an entry in the table, it returns # the associated value. No argument our call count constraints are # added. Using an expect_hash is identical to calling: # # mock.should_receive(method_name).and_return(value) # # for each of the method/value pairs in the hash. # # real_object :: # If a real object is given, then a partial mock is constructed # using the real_object as a base. Partial mocks (formally referred # to as stubs) behave as a mock object when an expectation is matched, # and otherwise will behave like the original object. This is useful # when you want to use a real object for testing, but need to mock out # just one or two methods. # # :base :: # Forces the following argument to be used as the base of a # partial mock object. This explicit tag is only needed if you # want to use a string or a symbol as the mock base (string and # symbols would normally be interpretted as the mock name). # # &block :: # If a block is given, then the mock object is passed to the block and # expectations may be configured within the block. When a block is given # for a partial mock, flexmock will return the domain object rather than # the mock object. # def flexmock(*args) name = nil quick_defs = {} domain_obj = nil safe_mode = false model_class = nil while ! args.empty? case args.first when :base, :safe safe_mode = (args.shift == :safe) domain_obj = args.shift when :model args.shift model_class = args.shift when String, Symbol name = args.shift.to_s when Hash quick_defs = args.shift else domain_obj = args.shift end end raise UsageError, "a block is required in safe mode" if safe_mode && ! block_given? if domain_obj mock = ContainerHelper.make_partial_proxy(self, domain_obj, name, safe_mode) result = domain_obj elsif model_class id = ContainerHelper.next_id result = mock = FlexMock.new("#{model_class}_#{id}", self) else result = mock = FlexMock.new(name || "unknown", self) end mock.should_receive(quick_defs) yield(mock) if block_given? flexmock_remember(mock) ContainerHelper.add_model_methods(mock, model_class, id) if model_class result end alias flexstub flexmock # Remember the mock object / stub in the mock container. def flexmock_remember(mocking_object) @flexmock_created_mocks ||= [] @flexmock_created_mocks << mocking_object mocking_object.flexmock_container = self mocking_object end end # ################################################################# # Helper methods for mock containers. MockContainer is a module # that is designed to be mixed into other classes, particularly # testing framework test cases. Since we don't want to pollute the # method namespace of the class that mixes in MockContainer, a # number of MockContainer methods were moved into ContainerHelper to # to isoloate the names. # class MockContainerHelper include FlexMock::ArgumentTypes # Return the next id for mocked models. def next_id @id_counter ||= 10000 @id_counter += 1 end # :call-seq: # parse_should_args(args) { |symbol| ... } # # This method provides common handling for the various should_receive # argument lists. It sorts out the differences between symbols, arrays and # hashes, and identifies the method names specified by each. As each # method name is identified, create a mock expectation for it using the # supplied block. def parse_should_args(mock, args, &block) # :nodoc: result = CompositeExpectation.new args.each do |arg| case arg when Hash arg.each do |k,v| exp = build_demeter_chain(mock, k, &block).and_return(v) result.add(exp) end when Symbol, String result.add(build_demeter_chain(mock, arg, &block)) end end result end # Automatically add mocks for some common methods in ActiveRecord # models. def add_model_methods(mock, model_class, id) container = mock.flexmock_container mock.should_receive( :id => id, :to_params => id.to_s, :new_record? => false, :class => model_class, :errors => container.flexmock("errors", :count => 0)) mock.should_receive(:is_a?).with(any).and_return { |other| other == model_class } mock.should_receive(:instance_of?).with(any).and_return { |other| other == model_class } mock.should_receive(:kind_of?).with(any).and_return { |other| model_class.ancestors.include?(other) } end # Create a PartialMockProxy for the given object. Use +name+ as # the name of the mock object. def make_partial_proxy(container, obj, name, safe_mode) name ||= "flexmock(#{obj.class.to_s})" obj.instance_eval { mock = FlexMock.new(name, container) @flexmock_proxy ||= PartialMockProxy.new(obj, mock, safe_mode) } obj.instance_variable_get("@flexmock_proxy") end private # Build the chain of mocks for demeter style mocking. # # Warning: Nasty code ahead. # # This method builds a chain of mocks to support demeter style # mocking. Given a mock chain of "first.second.third.last", we # must build a chain of mock methods that return the next mock in # the chain. The expectation for the last method of the chain is # returned as the result of the method. # # Things to consider: # # (1) The expectation for the "first" method must be created by # the proper mechanism, which is supplied by the block parameter # "block". In other words, first expectation is created by # calling the block. (This allows us to create expectations on # both pure mocks and partial mocks, with the block handling the # details). # # (2) Although the first mock is arbitrary, the remaining mocks in # the chain will always be pure mocks created specifically for # this purpose. # # (3) The expectations for all methods but the last in the chain # will be setup to expect no parameters and to return the next # mock in the chain. # # (4) It could very well be the case that several demeter chains # will be defined on a single mock object, and those chains could # share some of the same methods (e.g. "mock.one.two.read" and # "mock.one.two.write" both share the methods "one" and "two"). # It is important that the shared methods return the same mocks in # both chains. # def build_demeter_chain(mock, arg, &block) container = mock.flexmock_container names = arg.to_s.split('.') check_method_names(names) exp = nil next_exp = lambda { |n| block.call(n) } loop do method_name = names.shift.to_sym exp = mock.flexmock_find_expectation(method_name) need_new_exp = exp.nil? || names.empty? exp = next_exp.call(method_name) if need_new_exp break if names.empty? if need_new_exp mock = container.flexmock("demeter_#{method_name}") exp.with_no_args.and_return(mock) else mock = exp._return_value([]) end check_proper_mock(mock, method_name) next_exp = lambda { |n| mock.should_receive(n) } end exp end # Check that the given mock is a real FlexMock mock. def check_proper_mock(mock, method_name) unless mock.kind_of?(FlexMock) fail FlexMock::UsageError, "Conflicting mock declaration for '#{method_name}' in demeter style mock" end end METHOD_NAME_RE = /^([A-Za-z_][A-Za-z0-9_]*[=!?]?|\[\]=?||\*\*|<<|>>|<=>|[<>=]=|=~|===|[-+]@|[-+\*\/%&^|<>~])$/ # Check that all the names in the list are valid method names. def check_method_names(names) names.each do |name| fail FlexMock::UsageError, "Ill-formed method name '#{name}'" if name !~ METHOD_NAME_RE end end end ContainerHelper = MockContainerHelper.new end