module Hardmock
# Mock is used to set expectations in your test. Most of the time you'll use
# #expects to create expectations.
#
# Aside from the scant few control methods (like +expects+, +trap+ and +_verify+)
# all calls made on a Mock instance will be immediately applied to the internal
# expectation mechanism.
#
# * If the method call was expected and all the parameters match properly, execution continues
# * If the expectation was configured with an expectation block, the block is invoked
# * If the expectation was set up to raise an error, the error is raised now
# * If the expectation was set up to return a value, it is returned
# * If the method call was _not_ expected, or the parameter values are wrong, an ExpectationError is raised.
class Mock
include Hardmock::MethodCleanout
# Create a new Mock instance with a name and a MockControl to support it.
# If not given, a MockControl is made implicitly for this Mock alone; this means
# expectations for this mock are not tied to other expectations in your test.
#
# It's not recommended to use a Mock directly; see Hardmock and
# Hardmock#create_mocks for the more wholistic approach.
def initialize(name, mock_control=nil)
@name = name
@control = mock_control || MockControl.new
@expectation_builder = ExpectationBuilder.new
end
def inspect
""
end
# Begin declaring an expectation for this Mock.
#
# == Simple Examples
# Expect the +customer+ to be queried for +account+, and return "The
# Account":
# @customer.expects.account.returns "The Account"
#
# Expect the +withdraw+ method to be called, and raise an exception when it
# is (see Expectation#raises for more info):
# @cash_machine.expects.withdraw(20,:dollars).raises("not enough money")
#
# Expect +customer+ to have its +user_name+ set
# @customer.expects.user_name = 'Big Boss'
#
# Expect +customer+ to have its +user_name+ set, and raise a RuntimeException when
# that happens:
# @customer.expects('user_name=', "Big Boss").raises "lost connection"
#
# Expect +evaluate+ to be passed a block, and when that happens, pass a value
# to the block (see Expectation#yields for more info):
# @cruncher.expects.evaluate.yields("some data").returns("some results")
#
#
# == Expectation Blocks
# To do special handling of expected method calls when they occur, you
# may pass a block to your expectation, like:
# @page_scraper.expects.handle_content do |address,request,status|
# assert_not_nil address, "Can't abide nil addresses"
# assert_equal "http-get", request.method, "Can only handle GET"
# assert status > 200 and status < 300, status, "Failed status"
# "Simulated results #{request.content.downcase}"
# end
# In this example, when page_scraper.handle_content is called, its
# three arguments are passed to the expectation block and evaluated
# using the above assertions. The last value in the block will be used
# as the return value for +handle_content+
#
# You may specify arguments to the expected method call, just like any normal
# expectation, and those arguments will be pre-validated before being passed
# to the expectation block. This is useful when you know all of the
# expected values but still need to do something programmatic.
#
# If the method being invoked on the mock accepts a block, that block will be
# passed to your expectation block as the last (or only) argument. Eg, the
# convenience method +yields+ can be replaced with the more explicit:
# @cruncher.expects.evaluate do |block|
# block.call "some data"
# "some results"
# end
#
# The result value of the expectation block becomes the return value for the
# expected method call. This can be overidden by using the +returns+ method:
# @cruncher.expects.evaluate do |block|
# block.call "some data"
# "some results"
# end.returns("the actual value")
#
# Additionally, the resulting value of the expectation block is stored
# in the +block_value+ field on the expectation. If you've saved a reference
# to your expectation, you may retrieve the block value once the expectation
# has been met.
#
# evaluation_event = @cruncher.expects.evaluate do |block|
# block.call "some data"
# "some results"
# end.returns("the actual value")
#
# result = @cruncher.evaluate do |input|
# puts input # => 'some data'
# end
# # result is 'the actual value'
#
# evaluation_event.block_value # => 'some results'
#
def expects(*args, &block)
expector = Expector.new(self,@control,@expectation_builder)
# If there are no args, we return the Expector
return expector if args.empty?
# If there ARE args, we set up the expectation right here and return it
expector.send(args.shift.to_sym, *args, &block)
end
alias_method :expect, :expects
alias_method :should_receive, :expects
# Special-case convenience: #trap sets up an expectation for a method
# that will take a block. That block, when sent to the expected method, will
# be trapped and stored in the expectation's +block_value+ field.
# The Expectation#trigger method may then be used to invoke that block.
#
# Like +expects+, the +trap+ mechanism can be followed by +raises+ or +returns+.
#
# _Unlike_ +expects+, you may not use an expectation block with +trap+. If
# the expected method takes arguments in addition to the block, they must
# be specified in the arguments to the +trap+ call itself.
#
# == Example
#
# create_mocks :address_book, :editor_form
#
# # Expect a subscription on the :person_added event for @address_book:
# person_event = @address_book.trap.subscribe(:person_added)
#
# # The runtime code would look like:
# @address_book.subscribe :person_added do |person_name|
# @editor_form.name = person_name
# end
#
# # At this point, the expectation for 'subscribe' is met and the
# # block has been captured. But we're not done:
# @editor_form.expects.name = "David"
#
# # Now invoke the block we trapped earlier:
# person_event.trigger "David"
#
# verify_mocks
def trap(*args)
Trapper.new(self,@control,ExpectationBuilder.new)
end
def method_missing(mname,*args) #:nodoc:
block = nil
block = Proc.new if block_given?
@control.apply_method_call(self,mname,args,block)
end
def _control #:nodoc:
@control
end
def _name #:nodoc:
@name
end
# Verify that all expectations are fulfilled. NOTE: this method triggers
# validation on the _control_ for this mock, so all Mocks that share the
# MockControl with this instance will be included in the verification.
#
# Only use this method if you are managing your own Mocks and their controls.
#
# Normal usage of Hardmock doesn't require you to call this; let
# Hardmock#verify_mocks do it for you.
def _verify
@control.verify
end
end
end