hardmock 1.2.0 → 1.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,31 @@
1
+ require 'test/unit/assertions'
2
+ require 'hardmock/errors'
3
+
4
+ module Hardmock
5
+ class Trapper #:nodoc:
6
+ include Hardmock::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)
15
+ if block_given?
16
+ raise ExpectationError.new("Don't pass blocks when using 'trap' (setting exepectations for '#{mname}')")
17
+ end
18
+
19
+ the_block = lambda { |target_block| target_block }
20
+ expectation = @expectation_builder.build_expectation(
21
+ :mock => @mock,
22
+ :method => mname,
23
+ :arguments => args,
24
+ :suppress_arguments_to_block => true,
25
+ :block => the_block)
26
+
27
+ @mock_control.add_expectation expectation
28
+ expectation
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,9 @@
1
+
2
+ module Hardmock
3
+ module Utils #:nodoc:
4
+ def format_method_call_string(mock,mname,args)
5
+ arg_string = args.map { |a| a.inspect }.join(', ')
6
+ call_text = "#{mock._name}.#{mname}(#{arg_string})"
7
+ end
8
+ end
9
+ end
data/lib/hardmock.rb CHANGED
@@ -1,4 +1,12 @@
1
- require 'method_cleanout'
1
+ require 'hardmock/method_cleanout'
2
+ require 'hardmock/mock'
3
+ require 'hardmock/mock_control'
4
+ require 'hardmock/utils'
5
+ require 'hardmock/errors'
6
+ require 'hardmock/trapper'
7
+ require 'hardmock/expector'
8
+ require 'hardmock/expectation'
9
+ require 'hardmock/expectation_builder'
2
10
 
3
11
  module Hardmock
4
12
 
@@ -62,13 +70,14 @@ module Hardmock
62
70
  # The first call returned a hash { :donkey => @donkey, :cat => @cat }
63
71
  # and the second call returned { :dog => @dog }
64
72
  #
65
- # For more info on how to use your mocks, see Mock and SimpleExpectation
73
+ # For more info on how to use your mocks, see Mock and Expectation
66
74
  #
67
75
  def create_mocks(*mock_names)
68
76
  @main_mock_control ||= MockControl.new
69
77
 
70
78
  mocks = {}
71
79
  mock_names.each do |mock_name|
80
+ raise ArgumentError, "'nil' is not a valid name for a mock" if mock_name.nil?
72
81
  mock_name = mock_name.to_s
73
82
  mock_object = Mock.new(mock_name, @main_mock_control)
74
83
  mocks[mock_name.to_sym] = mock_object
@@ -94,534 +103,8 @@ module Hardmock
94
103
  @main_mock_control.verify
95
104
  end
96
105
 
97
- module Utils #:nodoc:
98
- def fmt_call(mock,mname,args)
99
- arg_string = args.map { |a| a.inspect }.join(', ')
100
- call_text = "#{mock._name}.#{mname}(#{arg_string})"
101
- end
102
- end
103
-
104
- # Mock is used to set expectations in your test. Most of the time you'll use
105
- # <tt>#expects</tt> to create expectations.
106
- #
107
- # Aside from the scant few control methods (like +expects+, +trap+ and +_verify+)
108
- # all calls made on a Mock instance will be immediately applied to the internal
109
- # expectation mechanism.
110
- #
111
- # * If the method call was expected and all the parameters match properly, execution continues
112
- # * If the expectation was configured with an expectation block, the block is invoked
113
- # * If the expectation was set up to raise an error, the error is raised now
114
- # * If the expectation was set up to return a value, it is returned
115
- # * If the method call was _not_ expected, or the parameter values are wrong, an ExpectationError is raised.
116
- class Mock
117
- include MethodCleanout
118
-
119
- # Create a new Mock instance with a name and a MockControl to support it.
120
- # If not given, a MockControl is made implicitly for this Mock alone; this means
121
- # expectations for this mock are not tied to other expectations in your test.
122
- #
123
- # It's not recommended to use a Mock directly; see Hardmock and
124
- # Hardmock#create_mocks for the more wholistic approach.
125
- def initialize(name, mock_control=nil)
126
- @name = name
127
- @control = mock_control || MockControl.new
128
- @expectation_builder = ExpectationBuilder.new
129
- end
130
-
131
- # Begin declaring an expectation for this Mock.
132
- #
133
- # == Simple Examples
134
- # Expect the +customer+ to be queried for +account+, and return <tt>"The
135
- # Account"</tt>:
136
- # @customer.expects.account.returns "The Account"
137
- #
138
- # Expect the +withdraw+ method to be called, and raise an exception when it
139
- # is (see SimpleExpectation#raises for more info):
140
- # @cash_machine.expects.withdraw(20,:dollars).raises("not enough money")
141
- #
142
- # Expect +customer+ to have its +user_name+ set
143
- # @customer.expects.user_name = 'Big Boss'
144
- #
145
- # Expect +customer+ to have its +user_name+ set, and raise a RuntimeException when
146
- # that happens:
147
- # @customer.expects('user_name=', "Big Boss").raises "lost connection"
148
- #
149
- # Expect +evaluate+ to be passed a block, and when that happens, pass a value
150
- # to the block (see SimpleExpectation#yields for more info):
151
- # @cruncher.expects.evaluate.yields("some data").returns("some results")
152
- #
153
- #
154
- # == Expectation Blocks
155
- # To do special handling of expected method calls when they occur, you
156
- # may pass a block to your expectation, like:
157
- # @page_scraper.expects.handle_content do |address,request,status|
158
- # assert_not_nil address, "Can't abide nil addresses"
159
- # assert_equal "http-get", request.method, "Can only handle GET"
160
- # assert status > 200 and status < 300, status, "Failed status"
161
- # "Simulated results #{request.content.downcase}"
162
- # end
163
- # In this example, when <tt>page_scraper.handle_content</tt> is called, its
164
- # three arguments are passed to the <i>expectation block</i> and evaluated
165
- # using the above assertions. The last value in the block will be used
166
- # as the return value for +handle_content+
167
- #
168
- # You may specify arguments to the expected method call, just like any normal
169
- # expectation, and those arguments will be pre-validated before being passed
170
- # to the expectation block. This is useful when you know all of the
171
- # expected values but still need to do something programmatic.
172
- #
173
- # If the method being invoked on the mock accepts a block, that block will be
174
- # passed to your expectation block as the last (or only) argument. Eg, the
175
- # convenience method +yields+ can be replaced with the more explicit:
176
- # @cruncher.expects.evaluate do |block|
177
- # block.call "some data"
178
- # "some results"
179
- # end
180
- #
181
- # The result value of the expectation block becomes the return value for the
182
- # expected method call. This can be overidden by using the +returns+ method:
183
- # @cruncher.expects.evaluate do |block|
184
- # block.call "some data"
185
- # "some results"
186
- # end.returns("the actual value")
187
- #
188
- # <b>Additionally</b>, the resulting value of the expectation block is stored
189
- # in the +block_value+ field on the expectation. If you've saved a reference
190
- # to your expectation, you may retrieve the block value once the expectation
191
- # has been met.
192
- #
193
- # evaluation_event = @cruncher.expects.evaluate do |block|
194
- # block.call "some data"
195
- # "some results"
196
- # end.returns("the actual value")
197
- #
198
- # result = @cruncher.evaluate do |input|
199
- # puts input # => 'some data'
200
- # end
201
- # # result is 'the actual value'
202
- #
203
- # evaluation_event.block_value # => 'some results'
204
- #
205
- def expects(*args, &block)
206
- expector = Expector.new(self,@control,@expectation_builder)
207
- # If there are no args, we return the Expector
208
- return expector if args.empty?
209
- # If there ARE args, we set up the expectation right here and return it
210
- expector.send(args.shift.to_sym, *args, &block)
211
- end
212
- alias_method :expect, :expects
213
- # def expect(*args, &block) #:nodoc:
214
- # raise DeprecationError.new("Please use 'expects' instead of 'expect'. Sorry about the inconvenience.")
215
- # end
216
-
217
- # Special-case convenience: #trap sets up an expectation for a method
218
- # that will take a block. That block, when sent to the expected method, will
219
- # be trapped and stored in the expectation's +block_value+ field.
220
- # The SimpleExpectation#trigger method may then be used to invoke that block.
221
- #
222
- # Like +expects+, the +trap+ mechanism can be followed by +raises+ or +returns+.
223
- #
224
- # _Unlike_ +expects+, you may not use an expectation block with +trap+. If
225
- # the expected method takes arguments in addition to the block, they must
226
- # be specified in the arguments to the +trap+ call itself.
227
- #
228
- # == Example
229
- #
230
- # create_mocks :address_book, :editor_form
231
- #
232
- # # Expect a subscription on the :person_added event for @address_book:
233
- # person_event = @address_book.trap.subscribe(:person_added)
234
- #
235
- # # The runtime code would look like:
236
- # @address_book.subscribe :person_added do |person_name|
237
- # @editor_form.name = person_name
238
- # end
239
- #
240
- # # At this point, the expectation for 'subscribe' is met and the
241
- # # block has been captured. But we're not done:
242
- # @editor_form.expects.name = "David"
243
- #
244
- # # Now invoke the block we trapped earlier:
245
- # person_event.trigger "David"
246
- #
247
- # verify_mocks
248
- def trap(*args)
249
- Trapper.new(self,@control,ExpectationBuilder.new)
250
- end
251
-
252
- def method_missing(mname,*args) #:nodoc:
253
- block = nil
254
- block = Proc.new if block_given?
255
- @control.apply_method_call(self,mname,args,block)
256
- end
257
-
258
-
259
- def _control #:nodoc:
260
- @control
261
- end
262
-
263
- def _name #:nodoc:
264
- @name
265
- end
266
-
267
- # Verify that all expectations are fulfilled. NOTE: this method triggers
268
- # validation on the _control_ for this mock, so all Mocks that share the
269
- # MockControl with this instance will be included in the verification.
270
- #
271
- # <b>Only use this method if you are managing your own Mocks and their controls.</b>
272
- #
273
- # Normal usage of Hardmock doesn't require you to call this; let
274
- # Hardmock#verify_mocks do it for you.
275
- def _verify
276
- @control.verify
277
- end
278
- end
279
-
280
- class Expector #:nodoc:
281
- include MethodCleanout
282
-
283
- def initialize(mock,mock_control,expectation_builder)
284
- @mock = mock
285
- @mock_control = mock_control
286
- @expectation_builder = expectation_builder
287
- end
288
-
289
- def method_missing(mname, *args, &block)
290
- expectation = @expectation_builder.build_expectation(
291
- :mock => @mock,
292
- :method => mname,
293
- :arguments => args,
294
- :block => block)
295
-
296
- @mock_control.add_expectation expectation
297
- expectation
298
- end
299
- end
300
-
301
- class Trapper #:nodoc:
302
- include MethodCleanout
303
-
304
- def initialize(mock,mock_control,expectation_builder)
305
- @mock = mock
306
- @mock_control = mock_control
307
- @expectation_builder = expectation_builder
308
- end
309
-
310
- def method_missing(mname, *args)
311
- if block_given?
312
- raise ExpectationError.new("Don't pass blocks when using 'trap' (setting exepectations for '#{mname}')")
313
- end
314
-
315
- the_block = lambda { |target_block| target_block }
316
- expectation = @expectation_builder.build_expectation(
317
- :mock => @mock,
318
- :method => mname,
319
- :arguments => args,
320
- :suppress_arguments_to_block => true,
321
- :block => the_block)
322
-
323
- @mock_control.add_expectation expectation
324
- expectation
325
- end
326
- end
327
-
328
- class ExpectationBuilder #:nodoc:
329
- def build_expectation(options)
330
- SimpleExpectation.new(options)
331
- end
332
- end
333
-
334
- class SimpleExpectation
335
- include Utils
336
- attr_reader :block_value
337
-
338
- def initialize(options) #:nodoc:
339
- @options = options
340
- end
341
-
342
- def apply_method_call(mock,mname,args,block) #:nodoc:
343
- unless @options[:mock].equal?(mock)
344
- raise anger("Wrong object", mock,mname,args)
345
- end
346
- unless @options[:method] == mname
347
- raise anger("Wrong method",mock,mname,args)
348
- end
349
-
350
- # Tester-defined block to invoke at method-call-time:
351
- expectation_block = @options[:block]
352
-
353
- expected_args = @options[:arguments]
354
- # if we have a block, we can skip the argument check if none were specified
355
- unless (expected_args.nil? || expected_args.empty?) && expectation_block && !@options[:suppress_arguments_to_block]
356
- unless expected_args == args
357
- raise anger("Wrong arguments",mock,mname,args)
358
- end
359
- end
360
-
361
- relayed_args = args.dup
362
- if block
363
- if expectation_block.nil?
364
- # Can't handle a runtime block without an expectation block
365
- raise ExpectationError.new("Unexpected block provided to #{to_s}")
366
- else
367
- # Runtime blocks are passed as final argument to the expectation block
368
- unless @options[:suppress_arguments_to_block]
369
- relayed_args << block
370
- else
371
- # Arguments suppressed; send only the block
372
- relayed_args = [block]
373
- end
374
- end
375
- end
376
-
377
- # Run the expectation block:
378
- @block_value = expectation_block.call(*relayed_args) if expectation_block
379
106
 
380
- raise @options[:raises] unless @options[:raises].nil?
381
-
382
- return_value = @options[:returns]
383
- if return_value.nil?
384
- return @block_value
385
- else
386
- return return_value
387
- end
388
- end
389
-
390
- # Set the return value for an expected method call.
391
- # Eg,
392
- # @cash_machine.expects.withdraw(20,:dollars).returns(20.00)
393
- def returns(val)
394
- @options[:returns] = val
395
- self
396
- end
397
-
398
- # Rig an expected method to raise an exception when the mock is invoked.
399
- #
400
- # Eg,
401
- # @cash_machine.expects.withdraw(20,:dollars).raises "Insufficient funds"
402
- #
403
- # The argument can be:
404
- # * an Exception -- will be used directly
405
- # * a String -- will be used as the message for a RuntimeError
406
- # * nothing -- RuntimeError.new("An Error") will be raised
407
- def raises(err=nil)
408
- case err
409
- when Exception
410
- @options[:raises] = err
411
- when String
412
- @options[:raises] = RuntimeError.new(err)
413
- else
414
- @options[:raises] = RuntimeError.new("An Error")
415
- end
416
- self
417
- end
418
-
419
- # Convenience method: assumes +block_value+ is set, and is set to a Proc
420
- # (or anything that responds to 'call')
421
- #
422
- # light_event = @traffic_light.trap.subscribe(:light_changes)
423
- #
424
- # # This code will meet the expectation:
425
- # @traffic_light.subscribe :light_changes do |color|
426
- # puts color
427
- # end
428
- #
429
- # The color-handling block is now stored in <tt>light_event.block_value</tt>
430
- #
431
- # The block can be invoked like this:
432
- #
433
- # light_event.trigger :red
434
- #
435
- # See Mock#trap and Mock#expects for information on using expectation objects
436
- # after they are set.
437
- #
438
- def trigger(*block_arguments)
439
- unless block_value
440
- raise ExpectationError.new("No block value is currently set for expectation #{to_s}")
441
- end
442
- unless block_value.respond_to?(:call)
443
- raise ExpectationError.new("Can't apply trigger to #{block_value} for expectation #{to_s}")
444
- end
445
- block_value.call *block_arguments
446
- end
447
-
448
- # Used when an expected method accepts a block at runtime.
449
- # When the expected method is invoked, the block passed to
450
- # that method will be invoked as well.
451
- #
452
- # NOTE: ExpectationError will be thrown upon running the expected method
453
- # if the arguments you set up in +yields+ do not properly match up with
454
- # the actual block that ends up getting passed.
455
- #
456
- # == Examples
457
- # <b>Single invocation</b>: The block passed to +lock_down+ gets invoked
458
- # once with no arguments:
459
- #
460
- # @safe_zone.expects.lock_down.yields
461
- #
462
- # # (works on code that looks like:)
463
- # @safe_zone.lock_down do
464
- # # ... this block invoked once
465
- # end
466
- #
467
- # <b>Multi-parameter blocks:</b> The block passed to +each_item+ gets
468
- # invoked twice, with <tt>:item1</tt> the first time, and with
469
- # <tt>:item2</tt> the second time:
470
- #
471
- # @fruit_basket.expects.each_with_index.yields [:apple,1], [:orange,2]
472
- #
473
- # # (works on code that looks like:)
474
- # @fruit_basket.each_with_index do |fruit,index|
475
- # # ... this block invoked with fruit=:apple, index=1,
476
- # # ... and then with fruit=:orange, index=2
477
- # end
478
- #
479
- # <b>Arrays can be passed as arguments too</b>... if the block
480
- # takes a single argument and you want to pass a series of arrays into it,
481
- # that will work as well:
482
- #
483
- # @list_provider.expects.each_list.yields [1,2,3], [4,5,6]
484
- #
485
- # # (works on code that looks like:)
486
- # @list_provider.each_list do |list|
487
- # # ... list is [1,2,3] the first time
488
- # # ... list is [4,5,6] the second time
489
- # end
490
- #
491
- # <b>Return value</b>: You can set the return value for the method that
492
- # accepts the block like so:
493
- #
494
- # @cruncher.expects.do_things.yields(:bean1,:bean2).returns("The Results")
495
- #
496
- # <b>Raising errors</b>: You can set the raised exception for the method that
497
- # accepts the block. NOTE: the error will be raised _after_ the block has
498
- # been invoked.
499
- #
500
- # # :bean1 and :bean2 will be passed to the block, then an error is raised:
501
- # @cruncher.expects.do_things.yields(:bean1,:bean2).raises("Too crunchy")
502
- #
503
- def yields(*items)
504
- @options[:suppress_arguments_to_block] = true
505
- if items.empty?
506
- # Yield once
507
- @options[:block] = lambda do |block|
508
- if block.arity != 0 and block.arity != -1
509
- raise ExpectationError.new("Can't pass #{item.inspect} to block with arity #{block.arity} to <#{to_s}>")
510
- end
511
- block.call
512
- end
513
- else
514
- # Yield one or more specific items
515
- @options[:block] = lambda do |block|
516
- items.each do |item|
517
- if item.kind_of?(Array)
518
- if block.arity == item.size
519
- # Unfold the array into the block's arguments:
520
- block.call *item
521
- elsif block.arity == 1
522
- # Just pass the array in
523
- block.call item
524
- else
525
- # Size mismatch
526
- raise ExpectationError.new("Can't pass #{item.inspect} to block with arity #{block.arity} to <#{to_s}>")
527
- end
528
- else
529
- if block.arity != 1
530
- # Size mismatch
531
- raise ExpectationError.new("Can't pass #{item.inspect} to block with arity #{block.arity} to <#{to_s}>")
532
- end
533
- block.call item
534
- end
535
- end
536
- end
537
- end
538
- self
539
- end
540
-
541
- def to_s # :nodoc:
542
- fmt_call(@options[:mock],@options[:method],@options[:arguments])
543
- end
544
-
545
- private
546
- def anger(msg, mock,mname,args)
547
- ExpectationError.new("#{msg}: expected call <#{to_s}> but was <#{fmt_call(mock,mname,args)}>")
548
- end
549
- end
550
107
 
551
- class MockControl #:nodoc:
552
- include Utils
553
- attr_accessor :name
554
-
555
- def initialize
556
- @expectations = []
557
- @disappointed = false
558
- end
559
-
560
- def happy?
561
- @expectations.empty?
562
- end
563
-
564
- def disappointed?
565
- @disappointed
566
- end
567
-
568
- def add_expectation(expectation)
569
- @expectations << expectation
570
- end
571
-
572
- def apply_method_call(mock,mname,args,block)
573
- # Are we even expecting any sort of call?
574
- if happy?
575
- @disappointed = true
576
- raise ExpectationError.new("Surprise call to #{fmt_call(mock,mname,args)}")
577
- end
578
-
579
- begin
580
- @expectations.shift.apply_method_call(mock,mname,args,block)
581
- rescue Exception => ouch
582
- @disappointed = true
583
- raise ouch
584
- end
585
- end
586
-
587
- def verify
588
- @disappointed = !happy?
589
- raise VerifyError.new("Unmet expectations", @expectations) unless happy?
590
- end
591
- end
592
-
593
- # Raised when:
594
- # * Unexpected method is called on a mock object
595
- # * Bad arguments passed to an expected call
596
- class ExpectationError < StandardError; end
597
-
598
- # Raised for methods that should no longer be called. Hopefully, the exception message contains helpful alternatives.
599
- class DeprecationError < StandardError; end
600
-
601
- # Raised when it is discovered that an expected method call was never made.
602
- class VerifyError < StandardError
603
- def initialize(msg,unmet_expectations)
604
- super("#{msg}:" + unmet_expectations.map { |ex| "\n * #{ex.to_s}" }.join)
605
- end
606
- end
607
-
608
- # A better 'assert_raise'. +patterns+ can be one or more Regexps, or a literal String that
609
- # must match the entire error message.
610
- def assert_error(err_type,*patterns,&block)
611
- assert_not_nil block, "assert_error requires a block"
612
- assert((err_type and err_type.kind_of?(Class)), "First argument to assert_error has to be an error type")
613
- err = assert_raise(err_type) do
614
- block.call
615
- end
616
- patterns.each do |pattern|
617
- case pattern
618
- when Regexp
619
- assert_match(pattern, err.message)
620
- else
621
- assert_equal pattern, err.message
622
- end
623
- end
624
- end
625
108
 
626
109
  end
627
110
 
@@ -0,0 +1,19 @@
1
+ require 'rake/rdoctask'
2
+ require File.expand_path(File.dirname(__FILE__) + "/rdoc_options.rb")
3
+
4
+ namespace :doc do
5
+
6
+ desc "Generate RDoc documentation"
7
+ Rake::RDocTask.new { |rdoc|
8
+ rdoc.rdoc_dir = 'doc'
9
+ rdoc.title = "Hardmock: Strict expectation-based mock object library "
10
+ add_rdoc_options(rdoc.options)
11
+ rdoc.rdoc_files.include('lib/**/*.rb', 'README','CHANGES','LICENSE')
12
+ }
13
+
14
+ task :show => [ 'doc:rerdoc' ] do
15
+ sh "open doc/index.html"
16
+ end
17
+
18
+ end
19
+
@@ -0,0 +1,4 @@
1
+
2
+ def add_rdoc_options(options)
3
+ options << '--line-numbers' << '--inline-source' << '--main' << 'README' << '--title' << 'Hardmock'
4
+ end
@@ -0,0 +1,22 @@
1
+ require 'rake/testtask'
2
+
3
+ namespace :test do
4
+
5
+ desc "Run unit tests"
6
+ Rake::TestTask.new("units") { |t|
7
+ t.libs << "test"
8
+ t.pattern = 'test/unit/*_test.rb'
9
+ t.verbose = true
10
+ }
11
+
12
+ desc "Run functional tests"
13
+ Rake::TestTask.new("functional") { |t|
14
+ t.libs << "test"
15
+ t.pattern = 'test/functional/*_test.rb'
16
+ t.verbose = true
17
+ }
18
+
19
+ desc "Run all the tests"
20
+ task :all => [ 'test:units', 'test:functional' ]
21
+
22
+ end
@@ -1,5 +1,5 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + "/../test_helper")
2
- require 'hardmock'
2
+ require 'assert_error'
3
3
 
4
4
  class AssertErrorTest < Test::Unit::TestCase
5
5