hardmock 1.2.0 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGES CHANGED
@@ -1,4 +1,29 @@
1
- Hardmock 1.0.3
1
+ Hardmock 1.2.3
2
+
3
+ Sat Apr 28 01:16:15 EDT 2007
4
+
5
+ * Re-release of 1.2.2 (which was canceled)... tasks moved to lib/tasks
6
+
7
+ Hardmock 1.2.2
8
+
9
+ Sat Apr 28 00:41:30 EDT 2007
10
+
11
+ * assert_error has been broken out into its own lib file
12
+ * Gem package can now run all tests successfully
13
+ * Internal code refactoring; a number of classes that were defined in hardmock.rb are now in their own files
14
+
15
+ Hardmock 1.2.1
16
+
17
+ Sat Apr 28 00:41:30 EDT 2007
18
+
19
+ * (botched release, see 1.2.2)
20
+
21
+ Hardmock 1.2.0
22
+
23
+ * You can now use "expect" in place of "expects" if you must.
24
+ * "inspect" has been added to the list of methods NOT erased by MethodCleanout.
25
+
26
+ Hardmock 1.1.0
2
27
 
3
28
  * "expects" replaces "expect" ("expect" now raises Hardmock::DeprecationError)
4
29
  * "verify_mocks" is now implicit in teardown, you needn't call it anymore
data/README CHANGED
@@ -7,7 +7,7 @@ Strict, ordered mock objects using very lightweight syntax in your tests.
7
7
 
8
8
  The basic procedure for using Hardmock in your tests is:
9
9
 
10
- * require 'hardmock'
10
+ * require 'hardmock' (this happens automatically when being used as a Rails plugin)
11
11
  * Create some mocks
12
12
  * Setup some expectations
13
13
  * Execute the target code
data/Rakefile CHANGED
@@ -1,94 +1,8 @@
1
1
  require 'rake'
2
- require 'rake/testtask'
3
- require 'rake/rdoctask'
4
2
  require 'rubygems'
5
- require 'rake/gempackagetask'
6
- require 'rake/contrib/sshpublisher'
7
- require 'hoe'
8
3
 
9
- HARDMOCK_VERSION = "1.2.0"
4
+ HARDMOCK_VERSION = "1.2.3"
10
5
 
11
- task :default => [ :alltests ]
6
+ Dir["lib/tasks/*.rake"].each { |f| load f }
12
7
 
13
- desc "Run all the tests"
14
- Rake::TestTask.new("alltests") { |t|
15
- t.libs << "test"
16
- t.pattern = 'test/**/*_test.rb'
17
- t.verbose = true
18
- }
19
-
20
- def add_rdoc_options(options)
21
- options << '--line-numbers' << '--inline-source' << '--main' << 'README' << '--title' << 'Hardmock'
22
- end
23
-
24
- desc "Generate RDoc documentation"
25
- Rake::RDocTask.new { |rdoc|
26
- rdoc.rdoc_dir = 'doc'
27
- rdoc.title = "Hardmock: Strict expectation-based mock object library "
28
- # rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object' << '--main'
29
- add_rdoc_options(rdoc.options)
30
- rdoc.rdoc_files.include('lib/**/*.rb', 'README','CHANGES','LICENSE')
31
- }
32
-
33
- task :showdoc => [ :rerdoc ] do
34
- sh "open doc/index.html"
35
- end
36
-
37
- desc "Generate and upload api docs to rubyforge"
38
- task :upload_doc => :rerdoc do
39
- sh "scp -r doc/* rubyforge.org:/var/www/gforge-projects/hardmock/doc"
40
- sh "scp -r homepage/* rubyforge.org:/var/www/gforge-projects/hardmock/"
41
- end
42
-
43
-
44
-
45
- gem_spec = Gem::Specification.new do | s |
46
- s.name = "hardmock"
47
- s.version = HARDMOCK_VERSION
48
- s.author = "David Crosby"
49
- s.email = "crosby@atomicobject.com"
50
- s.platform = Gem::Platform::RUBY
51
- s.summary = "A strict, ordered, expectation-oriented mock object library."
52
- s.rubyforge_project = 'hardmock'
53
- s.homepage = "http://hardmock.rubyforge.org"
54
- s.autorequire = 'hardmock'
55
-
56
- s.files = FileList['{lib,test}/**/*.rb', '[A-Z]*'].exclude('TODO').to_a
57
-
58
- s.require_path = "lib"
59
- s.test_files = Dir.glob("test/**/*test.rb")
60
-
61
- s.has_rdoc = true
62
- s.extra_rdoc_files = ["README","CHANGES","LICENSE"]
63
- add_rdoc_options(s.rdoc_options)
64
- end
65
-
66
- Rake::GemPackageTask.new(gem_spec) do |pkg|
67
- pkg.need_zip = true
68
- pkg.need_tar = true
69
- end
70
-
71
- task :verify_svn_clean do
72
- # Get clean
73
- sh 'svn up'
74
- status = `svn status`
75
- raise "Please get checked-in and cleaned up before releasing.\n#{status}" unless status == ""
76
- end
77
-
78
- desc "Create a release tar.gz file."
79
- task :release => [:verify_svn_clean, :alltests, :upload_doc, :repackage] do
80
- require 'fileutils'
81
- include FileUtils::Verbose
82
- proj_root = File.expand_path(File.dirname(__FILE__))
83
- begin
84
- cd proj_root
85
-
86
-
87
- # Tag the release by number, then re-tag for stable release (makes nicey nicey for Rails plugin installation)
88
- sh "svn cp . svn+ssh://dcrosby42@rubyforge.org/var/svn/hardmock/tags/rel-#{HARDMOCK_VERSION} -m 'Releasing version #{HARDMOCK_VERSION}'"
89
- sh "svn del svn+ssh://dcrosby42@rubyforge.org/var/svn/hardmock/tags/hardmock -m 'Preparing to update stable release tag'"
90
- sh "svn cp . svn+ssh://dcrosby42@rubyforge.org/var/svn/hardmock/tags/hardmock -m 'Updating stable tag to version #{HARDMOCK_VERSION}'"
91
-
92
- puts "UPLOAD #{Dir['pkg/*.*']} TO RUBYFORGE RELEASE ZONE"
93
- end
94
- end
8
+ task :default => [ 'test:all' ]
@@ -0,0 +1,12 @@
1
+ # The path to the root directory of your application.
2
+ APP_ROOT = File.join(File.dirname(__FILE__), '..')
3
+
4
+ ADDITIONAL_LOAD_PATHS = []
5
+ ADDITIONAL_LOAD_PATHS.concat %w(
6
+ lib
7
+ ).map { |dir| "#{APP_ROOT}/#{dir}" }.select { |dir| File.directory?(dir) }
8
+
9
+ # Prepend to $LOAD_PATH
10
+ ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) }
11
+
12
+ # Require any additional libraries needed
@@ -0,0 +1,21 @@
1
+ require 'test/unit/assertions'
2
+
3
+ module Test::Unit::Assertions
4
+ # A better 'assert_raise'. +patterns+ can be one or more Regexps, or a literal String that
5
+ # must match the entire error message.
6
+ def assert_error(err_type,*patterns,&block)
7
+ assert_not_nil block, "assert_error requires a block"
8
+ assert((err_type and err_type.kind_of?(Class)), "First argument to assert_error has to be an error type")
9
+ err = assert_raise(err_type) do
10
+ block.call
11
+ end
12
+ patterns.each do |pattern|
13
+ case pattern
14
+ when Regexp
15
+ assert_match(pattern, err.message)
16
+ else
17
+ assert_equal pattern, err.message
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,16 @@
1
+ module Hardmock
2
+ # Raised when:
3
+ # * Unexpected method is called on a mock object
4
+ # * Bad arguments passed to an expected call
5
+ class ExpectationError < StandardError; end
6
+
7
+ # Raised for methods that should no longer be called. Hopefully, the exception message contains helpful alternatives.
8
+ class DeprecationError < StandardError; end
9
+
10
+ # Raised when it is discovered that an expected method call was never made.
11
+ class VerifyError < StandardError
12
+ def initialize(msg,unmet_expectations)
13
+ super("#{msg}:" + unmet_expectations.map { |ex| "\n * #{ex.to_s}" }.join)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,220 @@
1
+ require 'hardmock/utils'
2
+
3
+ module Hardmock
4
+ class Expectation
5
+ include Utils
6
+ attr_reader :block_value
7
+
8
+ def initialize(options) #:nodoc:
9
+ @options = options
10
+ end
11
+
12
+ def apply_method_call(mock,mname,args,block) #:nodoc:
13
+ unless @options[:mock].equal?(mock)
14
+ raise anger("Wrong object", mock,mname,args)
15
+ end
16
+ unless @options[:method] == mname
17
+ raise anger("Wrong method",mock,mname,args)
18
+ end
19
+
20
+ # Tester-defined block to invoke at method-call-time:
21
+ expectation_block = @options[:block]
22
+
23
+ expected_args = @options[:arguments]
24
+ # if we have a block, we can skip the argument check if none were specified
25
+ unless (expected_args.nil? || expected_args.empty?) && expectation_block && !@options[:suppress_arguments_to_block]
26
+ unless expected_args == args
27
+ raise anger("Wrong arguments",mock,mname,args)
28
+ end
29
+ end
30
+
31
+ relayed_args = args.dup
32
+ if block
33
+ if expectation_block.nil?
34
+ # Can't handle a runtime block without an expectation block
35
+ raise ExpectationError.new("Unexpected block provided to #{to_s}")
36
+ else
37
+ # Runtime blocks are passed as final argument to the expectation block
38
+ unless @options[:suppress_arguments_to_block]
39
+ relayed_args << block
40
+ else
41
+ # Arguments suppressed; send only the block
42
+ relayed_args = [block]
43
+ end
44
+ end
45
+ end
46
+
47
+ # Run the expectation block:
48
+ @block_value = expectation_block.call(*relayed_args) if expectation_block
49
+
50
+ raise @options[:raises] unless @options[:raises].nil?
51
+
52
+ return_value = @options[:returns]
53
+ if return_value.nil?
54
+ return @block_value
55
+ else
56
+ return return_value
57
+ end
58
+ end
59
+
60
+ # Set the return value for an expected method call.
61
+ # Eg,
62
+ # @cash_machine.expects.withdraw(20,:dollars).returns(20.00)
63
+ def returns(val)
64
+ @options[:returns] = val
65
+ self
66
+ end
67
+
68
+ # Rig an expected method to raise an exception when the mock is invoked.
69
+ #
70
+ # Eg,
71
+ # @cash_machine.expects.withdraw(20,:dollars).raises "Insufficient funds"
72
+ #
73
+ # The argument can be:
74
+ # * an Exception -- will be used directly
75
+ # * a String -- will be used as the message for a RuntimeError
76
+ # * nothing -- RuntimeError.new("An Error") will be raised
77
+ def raises(err=nil)
78
+ case err
79
+ when Exception
80
+ @options[:raises] = err
81
+ when String
82
+ @options[:raises] = RuntimeError.new(err)
83
+ else
84
+ @options[:raises] = RuntimeError.new("An Error")
85
+ end
86
+ self
87
+ end
88
+
89
+ # Convenience method: assumes +block_value+ is set, and is set to a Proc
90
+ # (or anything that responds to 'call')
91
+ #
92
+ # light_event = @traffic_light.trap.subscribe(:light_changes)
93
+ #
94
+ # # This code will meet the expectation:
95
+ # @traffic_light.subscribe :light_changes do |color|
96
+ # puts color
97
+ # end
98
+ #
99
+ # The color-handling block is now stored in <tt>light_event.block_value</tt>
100
+ #
101
+ # The block can be invoked like this:
102
+ #
103
+ # light_event.trigger :red
104
+ #
105
+ # See Mock#trap and Mock#expects for information on using expectation objects
106
+ # after they are set.
107
+ #
108
+ def trigger(*block_arguments)
109
+ unless block_value
110
+ raise ExpectationError.new("No block value is currently set for expectation #{to_s}")
111
+ end
112
+ unless block_value.respond_to?(:call)
113
+ raise ExpectationError.new("Can't apply trigger to #{block_value} for expectation #{to_s}")
114
+ end
115
+ block_value.call *block_arguments
116
+ end
117
+
118
+ # Used when an expected method accepts a block at runtime.
119
+ # When the expected method is invoked, the block passed to
120
+ # that method will be invoked as well.
121
+ #
122
+ # NOTE: ExpectationError will be thrown upon running the expected method
123
+ # if the arguments you set up in +yields+ do not properly match up with
124
+ # the actual block that ends up getting passed.
125
+ #
126
+ # == Examples
127
+ # <b>Single invocation</b>: The block passed to +lock_down+ gets invoked
128
+ # once with no arguments:
129
+ #
130
+ # @safe_zone.expects.lock_down.yields
131
+ #
132
+ # # (works on code that looks like:)
133
+ # @safe_zone.lock_down do
134
+ # # ... this block invoked once
135
+ # end
136
+ #
137
+ # <b>Multi-parameter blocks:</b> The block passed to +each_item+ gets
138
+ # invoked twice, with <tt>:item1</tt> the first time, and with
139
+ # <tt>:item2</tt> the second time:
140
+ #
141
+ # @fruit_basket.expects.each_with_index.yields [:apple,1], [:orange,2]
142
+ #
143
+ # # (works on code that looks like:)
144
+ # @fruit_basket.each_with_index do |fruit,index|
145
+ # # ... this block invoked with fruit=:apple, index=1,
146
+ # # ... and then with fruit=:orange, index=2
147
+ # end
148
+ #
149
+ # <b>Arrays can be passed as arguments too</b>... if the block
150
+ # takes a single argument and you want to pass a series of arrays into it,
151
+ # that will work as well:
152
+ #
153
+ # @list_provider.expects.each_list.yields [1,2,3], [4,5,6]
154
+ #
155
+ # # (works on code that looks like:)
156
+ # @list_provider.each_list do |list|
157
+ # # ... list is [1,2,3] the first time
158
+ # # ... list is [4,5,6] the second time
159
+ # end
160
+ #
161
+ # <b>Return value</b>: You can set the return value for the method that
162
+ # accepts the block like so:
163
+ #
164
+ # @cruncher.expects.do_things.yields(:bean1,:bean2).returns("The Results")
165
+ #
166
+ # <b>Raising errors</b>: You can set the raised exception for the method that
167
+ # accepts the block. NOTE: the error will be raised _after_ the block has
168
+ # been invoked.
169
+ #
170
+ # # :bean1 and :bean2 will be passed to the block, then an error is raised:
171
+ # @cruncher.expects.do_things.yields(:bean1,:bean2).raises("Too crunchy")
172
+ #
173
+ def yields(*items)
174
+ @options[:suppress_arguments_to_block] = true
175
+ if items.empty?
176
+ # Yield once
177
+ @options[:block] = lambda do |block|
178
+ if block.arity != 0 and block.arity != -1
179
+ raise ExpectationError.new("Can't pass #{item.inspect} to block with arity #{block.arity} to <#{to_s}>")
180
+ end
181
+ block.call
182
+ end
183
+ else
184
+ # Yield one or more specific items
185
+ @options[:block] = lambda do |block|
186
+ items.each do |item|
187
+ if item.kind_of?(Array)
188
+ if block.arity == item.size
189
+ # Unfold the array into the block's arguments:
190
+ block.call *item
191
+ elsif block.arity == 1
192
+ # Just pass the array in
193
+ block.call item
194
+ else
195
+ # Size mismatch
196
+ raise ExpectationError.new("Can't pass #{item.inspect} to block with arity #{block.arity} to <#{to_s}>")
197
+ end
198
+ else
199
+ if block.arity != 1
200
+ # Size mismatch
201
+ raise ExpectationError.new("Can't pass #{item.inspect} to block with arity #{block.arity} to <#{to_s}>")
202
+ end
203
+ block.call item
204
+ end
205
+ end
206
+ end
207
+ end
208
+ self
209
+ end
210
+
211
+ def to_s # :nodoc:
212
+ format_method_call_string(@options[:mock],@options[:method],@options[:arguments])
213
+ end
214
+
215
+ private
216
+ def anger(msg, mock,mname,args)
217
+ ExpectationError.new("#{msg}: expected call <#{to_s}> but was <#{format_method_call_string(mock,mname,args)}>")
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,9 @@
1
+ require 'hardmock/expectation'
2
+
3
+ module Hardmock
4
+ class ExpectationBuilder #:nodoc:
5
+ def build_expectation(options)
6
+ Expectation.new(options)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,26 @@
1
+ require 'hardmock/method_cleanout'
2
+ require 'hardmock/errors'
3
+
4
+ module Hardmock
5
+ class Expector #:nodoc:
6
+ include MethodCleanout
7
+
8
+ def initialize(mock,mock_control,expectation_builder)
9
+ @mock = mock
10
+ @mock_control = mock_control
11
+ @expectation_builder = expectation_builder
12
+ end
13
+
14
+ def method_missing(mname, *args, &block)
15
+ expectation = @expectation_builder.build_expectation(
16
+ :mock => @mock,
17
+ :method => mname,
18
+ :arguments => args,
19
+ :block => block)
20
+
21
+ @mock_control.add_expectation expectation
22
+ expectation
23
+ end
24
+ end
25
+
26
+ end
@@ -1,7 +1,7 @@
1
1
 
2
2
  module Hardmock #:nodoc:
3
3
  module MethodCleanout #:nodoc:
4
- SACRED_METHODS = %w|__id__ __send__ equal? object_id send nil? class kind_of? respond_to? inspect|
4
+ SACRED_METHODS = %w|__id__ __send__ equal? object_id send nil? class kind_of? respond_to? inspect method to_s instance_variables instance_eval ==|
5
5
 
6
6
  def self.included(base) #:nodoc:
7
7
  base.class_eval do
@@ -0,0 +1,182 @@
1
+
2
+ module Hardmock
3
+ # Mock is used to set expectations in your test. Most of the time you'll use
4
+ # <tt>#expects</tt> to create expectations.
5
+ #
6
+ # Aside from the scant few control methods (like +expects+, +trap+ and +_verify+)
7
+ # all calls made on a Mock instance will be immediately applied to the internal
8
+ # expectation mechanism.
9
+ #
10
+ # * If the method call was expected and all the parameters match properly, execution continues
11
+ # * If the expectation was configured with an expectation block, the block is invoked
12
+ # * If the expectation was set up to raise an error, the error is raised now
13
+ # * If the expectation was set up to return a value, it is returned
14
+ # * If the method call was _not_ expected, or the parameter values are wrong, an ExpectationError is raised.
15
+ class Mock
16
+ include Hardmock::MethodCleanout
17
+
18
+ # Create a new Mock instance with a name and a MockControl to support it.
19
+ # If not given, a MockControl is made implicitly for this Mock alone; this means
20
+ # expectations for this mock are not tied to other expectations in your test.
21
+ #
22
+ # It's not recommended to use a Mock directly; see Hardmock and
23
+ # Hardmock#create_mocks for the more wholistic approach.
24
+ def initialize(name, mock_control=nil)
25
+ @name = name
26
+ @control = mock_control || MockControl.new
27
+ @expectation_builder = ExpectationBuilder.new
28
+ end
29
+
30
+ def inspect
31
+ "<Mock #{@name}>"
32
+ end
33
+
34
+ # Begin declaring an expectation for this Mock.
35
+ #
36
+ # == Simple Examples
37
+ # Expect the +customer+ to be queried for +account+, and return <tt>"The
38
+ # Account"</tt>:
39
+ # @customer.expects.account.returns "The Account"
40
+ #
41
+ # Expect the +withdraw+ method to be called, and raise an exception when it
42
+ # is (see Expectation#raises for more info):
43
+ # @cash_machine.expects.withdraw(20,:dollars).raises("not enough money")
44
+ #
45
+ # Expect +customer+ to have its +user_name+ set
46
+ # @customer.expects.user_name = 'Big Boss'
47
+ #
48
+ # Expect +customer+ to have its +user_name+ set, and raise a RuntimeException when
49
+ # that happens:
50
+ # @customer.expects('user_name=', "Big Boss").raises "lost connection"
51
+ #
52
+ # Expect +evaluate+ to be passed a block, and when that happens, pass a value
53
+ # to the block (see Expectation#yields for more info):
54
+ # @cruncher.expects.evaluate.yields("some data").returns("some results")
55
+ #
56
+ #
57
+ # == Expectation Blocks
58
+ # To do special handling of expected method calls when they occur, you
59
+ # may pass a block to your expectation, like:
60
+ # @page_scraper.expects.handle_content do |address,request,status|
61
+ # assert_not_nil address, "Can't abide nil addresses"
62
+ # assert_equal "http-get", request.method, "Can only handle GET"
63
+ # assert status > 200 and status < 300, status, "Failed status"
64
+ # "Simulated results #{request.content.downcase}"
65
+ # end
66
+ # In this example, when <tt>page_scraper.handle_content</tt> is called, its
67
+ # three arguments are passed to the <i>expectation block</i> and evaluated
68
+ # using the above assertions. The last value in the block will be used
69
+ # as the return value for +handle_content+
70
+ #
71
+ # You may specify arguments to the expected method call, just like any normal
72
+ # expectation, and those arguments will be pre-validated before being passed
73
+ # to the expectation block. This is useful when you know all of the
74
+ # expected values but still need to do something programmatic.
75
+ #
76
+ # If the method being invoked on the mock accepts a block, that block will be
77
+ # passed to your expectation block as the last (or only) argument. Eg, the
78
+ # convenience method +yields+ can be replaced with the more explicit:
79
+ # @cruncher.expects.evaluate do |block|
80
+ # block.call "some data"
81
+ # "some results"
82
+ # end
83
+ #
84
+ # The result value of the expectation block becomes the return value for the
85
+ # expected method call. This can be overidden by using the +returns+ method:
86
+ # @cruncher.expects.evaluate do |block|
87
+ # block.call "some data"
88
+ # "some results"
89
+ # end.returns("the actual value")
90
+ #
91
+ # <b>Additionally</b>, the resulting value of the expectation block is stored
92
+ # in the +block_value+ field on the expectation. If you've saved a reference
93
+ # to your expectation, you may retrieve the block value once the expectation
94
+ # has been met.
95
+ #
96
+ # evaluation_event = @cruncher.expects.evaluate do |block|
97
+ # block.call "some data"
98
+ # "some results"
99
+ # end.returns("the actual value")
100
+ #
101
+ # result = @cruncher.evaluate do |input|
102
+ # puts input # => 'some data'
103
+ # end
104
+ # # result is 'the actual value'
105
+ #
106
+ # evaluation_event.block_value # => 'some results'
107
+ #
108
+ def expects(*args, &block)
109
+ expector = Expector.new(self,@control,@expectation_builder)
110
+ # If there are no args, we return the Expector
111
+ return expector if args.empty?
112
+ # If there ARE args, we set up the expectation right here and return it
113
+ expector.send(args.shift.to_sym, *args, &block)
114
+ end
115
+ alias_method :expect, :expects
116
+ # def expect(*args, &block) #:nodoc:
117
+ # raise DeprecationError.new("Please use 'expects' instead of 'expect'. Sorry about the inconvenience.")
118
+ # end
119
+
120
+ # Special-case convenience: #trap sets up an expectation for a method
121
+ # that will take a block. That block, when sent to the expected method, will
122
+ # be trapped and stored in the expectation's +block_value+ field.
123
+ # The Expectation#trigger method may then be used to invoke that block.
124
+ #
125
+ # Like +expects+, the +trap+ mechanism can be followed by +raises+ or +returns+.
126
+ #
127
+ # _Unlike_ +expects+, you may not use an expectation block with +trap+. If
128
+ # the expected method takes arguments in addition to the block, they must
129
+ # be specified in the arguments to the +trap+ call itself.
130
+ #
131
+ # == Example
132
+ #
133
+ # create_mocks :address_book, :editor_form
134
+ #
135
+ # # Expect a subscription on the :person_added event for @address_book:
136
+ # person_event = @address_book.trap.subscribe(:person_added)
137
+ #
138
+ # # The runtime code would look like:
139
+ # @address_book.subscribe :person_added do |person_name|
140
+ # @editor_form.name = person_name
141
+ # end
142
+ #
143
+ # # At this point, the expectation for 'subscribe' is met and the
144
+ # # block has been captured. But we're not done:
145
+ # @editor_form.expects.name = "David"
146
+ #
147
+ # # Now invoke the block we trapped earlier:
148
+ # person_event.trigger "David"
149
+ #
150
+ # verify_mocks
151
+ def trap(*args)
152
+ Trapper.new(self,@control,ExpectationBuilder.new)
153
+ end
154
+
155
+ def method_missing(mname,*args) #:nodoc:
156
+ block = nil
157
+ block = Proc.new if block_given?
158
+ @control.apply_method_call(self,mname,args,block)
159
+ end
160
+
161
+
162
+ def _control #:nodoc:
163
+ @control
164
+ end
165
+
166
+ def _name #:nodoc:
167
+ @name
168
+ end
169
+
170
+ # Verify that all expectations are fulfilled. NOTE: this method triggers
171
+ # validation on the _control_ for this mock, so all Mocks that share the
172
+ # MockControl with this instance will be included in the verification.
173
+ #
174
+ # <b>Only use this method if you are managing your own Mocks and their controls.</b>
175
+ #
176
+ # Normal usage of Hardmock doesn't require you to call this; let
177
+ # Hardmock#verify_mocks do it for you.
178
+ def _verify
179
+ @control.verify
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,45 @@
1
+ require 'hardmock/utils'
2
+
3
+ module Hardmock
4
+ class MockControl #:nodoc:
5
+ include Utils
6
+ attr_accessor :name
7
+
8
+ def initialize
9
+ @expectations = []
10
+ @disappointed = false
11
+ end
12
+
13
+ def happy?
14
+ @expectations.empty?
15
+ end
16
+
17
+ def disappointed?
18
+ @disappointed
19
+ end
20
+
21
+ def add_expectation(expectation)
22
+ @expectations << expectation
23
+ end
24
+
25
+ def apply_method_call(mock,mname,args,block)
26
+ # Are we even expecting any sort of call?
27
+ if happy?
28
+ @disappointed = true
29
+ raise ExpectationError.new("Surprise call to #{format_method_call_string(mock,mname,args)}")
30
+ end
31
+
32
+ begin
33
+ @expectations.shift.apply_method_call(mock,mname,args,block)
34
+ rescue Exception => ouch
35
+ @disappointed = true
36
+ raise ouch
37
+ end
38
+ end
39
+
40
+ def verify
41
+ @disappointed = !happy?
42
+ raise VerifyError.new("Unmet expectations", @expectations) unless happy?
43
+ end
44
+ end
45
+ end