File: partial_mock.rb

package info (click to toggle)
ruby-flexmock 3.0.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 836 kB
  • sloc: ruby: 7,572; makefile: 6
file content (475 lines) | stat: -rw-r--r-- 14,714 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
#!/usr/bin/env ruby

#---
# Copyright 2003-2013 by Jim Weirich (jim.weirich@gmail.com).
# 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/expectation_builder'

class FlexMock

  # PartialMockProxy is used to mate the mock framework to an existing
  # object. The object is "enhanced" with a reference to a mock object
  # (stored in <tt>@flexmock_proxy</tt>). When the +should_receive+
  # method is sent to the proxy, it overrides the existing object's
  # method by creating singleton method that forwards to the mock.
  # When testing is complete, PartialMockProxy will erase the mocking
  # infrastructure from the object being mocked (e.g. remove instance
  # variables and mock singleton methods).
  #
  class PartialMockProxy
    include Ordering

    attr_reader :mock

    # Boxing of the flexmock proxy
    #
    # It is managed as a stack in order to allow to setup containers recursively
    # (as e.g. FlexMock.use( ... ) checks)
    class ProxyBox
      attr_reader :stack

      Element = Struct.new :proxy, :container

      def initialize
        @stack = [Element.new]
      end

      # Tests whether the given container is the one on which the current proxy
      # acts
      def container
        stack.last.container
      end

      def proxy
        stack.last.proxy
      end

      def push(proxy, container)
        stack.push(Element.new(proxy, container))
      end

      def pop
        if !stack.empty?
          stack.pop
        end
      end

      def empty?
        stack.size == 1
      end
    end

    # Make a partial mock proxy and install it on the target +obj+.
    def self.make_proxy_for(obj, container, name, safe_mode)
      name ||= "flexmock(#{obj.class.to_s})"
      if !obj.instance_variable_defined?("@flexmock_proxy")
        proxy_box = obj.instance_variable_set("@flexmock_proxy", ProxyBox.new)
      else
        proxy_box = obj.instance_variable_get("@flexmock_proxy")
      end

      if proxy_box.container != container
        if !proxy_box.empty?
          parent_proxy, _ = proxy_box.proxy
          parent_mock = parent_proxy.mock
        end

        mock  = FlexMock.new(name, container, parent: parent_mock)
        proxy = PartialMockProxy.new(obj, mock, safe_mode, parent: parent_proxy)
        proxy_box.push(proxy, container)
      end
      proxy_box.proxy
    end

    # The following methods are added to partial mocks so that they
    # can act like a mock.

    MOCK_METHODS = [
      :should_receive, :new_instances, :should_expect,
      :should_receive_with_location,
      :flexmock_get,   :flexmock_teardown, :flexmock_verify,
      :flexmock_received?, :flexmock_calls, :flexmock_find_expectation,
      :invoke_original
    ]

    # Initialize a PartialMockProxy object.
    def initialize(obj, mock, safe_mode, parent: nil)
      @obj = obj
      @mock = mock
      @proxy_definition_module = nil
      @parent = parent
      @initialize_override = nil

      unless safe_mode
        add_mock_method(:should_receive)
        MOCK_METHODS.each do |sym|
          unless @obj.respond_to?(sym)
            add_mock_method(sym)
          end
        end
      end
    end

    # Get the mock object for the partial mock.
    def flexmock_get
      @mock
    end

    def push_flexmock_container(container)
      @mock.push_flexmock_container(container)
    end

    def pop_flexmock_container
      @mock.pop_flexmock_container
    end

    # :call-seq:
    #    should_receive(:method_name)
    #    should_receive(:method1, method2, ...)
    #    should_receive(:meth1 => result1, :meth2 => result2, ...)
    #
    # Declare that the partial mock should receive a message with the given
    # name.
    #
    # If more than one method name is given, then the mock object should
    # expect to receive all the listed melthods.  If a hash of method
    # name/value pairs is given, then the each method will return the
    # associated result.  Any expectations applied to the result of
    # +should_receive+ will be applied to all the methods defined in the
    # argument list.
    #
    # 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(*args, **kw)
      flexmock_define_expectation(caller, *args, **kw)
    end

    def should_expect(*args)
      yield Recorder.new(self)
    end

    # Invoke the original of a mocked method
    #
    # Usually called in a #and_return statement
    def invoke_original(m, *args, **kw, &block)
      flexmock_invoke_original(m, args, kw, block)
    end

    # Whether the given method's original definition has been stored
    def find_original_method(m)
      it =
        if m.respond_to?(:to_str) || m.respond_to?(:to_sym)
          @obj.method(m)
        else
          m
        end

      while it && (it.owner != @proxy_definition_module)
        it = it.super_method
      end

      return unless it
      while it && it.owner.kind_of?(ProxyDefinitionModule)
        it = it.super_method
      end
      it
    rescue NameError => e
      raise unless e.name == m
    end

    # Whether the given method's original definition has been stored
    def original_method(m)
      unless (m = find_original_method(m))
        raise ArgumentError, "no original method for #{m}"
      end
      m
    end

    # Whether the given method's original definition has been stored
    def has_original_method?(m)
      find_original_method(m)
    end

    # Whether the given method is already being proxied
    def has_proxied_method?(m)
      @proxy_definition_module &&
          @proxy_definition_module.method_defined?(m)
    end

    def flexmock_plain_new_method?(m)
      m.name == :new && m.owner == Class
    end

    def flexmock_define_expectation(location, *args, **kw)
      EXP_BUILDER.parse_should_args(self, args, kw) do |method_name|
        if !has_proxied_method?(method_name)
          define_proxy_method(method_name)
        end
        ex = @mock.flexmock_define_expectation(location, method_name)
        if FlexMock.partials_verify_signatures
          if (existing_method = find_original_method(method_name))
            if flexmock_plain_new_method?(existing_method)
              # Look for the signature of `initialize` instead
              ex.with_signature_matching(@obj.instance_method(:initialize))
            else
              ex.with_signature_matching(existing_method)
            end
          end
        end
        ex.mock = self
        ex
      end
    end

    def flexmock_find_expectation(*args, **kw, &block)
      @mock.flexmock_find_expectation(*args, **kw, &block)
    end

    def add_mock_method(method_name)
      proxy_module_eval do
        define_method(method_name) { |*args, **kw, &block|
          proxy = __flexmock_proxy or
            fail "Missing FlexMock proxy " +
                 "(for method_name=#{method_name.inspect}, self=\#{self})"
          proxy.send(method_name, *args, **kw, &block)
        }
      end
    end

    # :call-seq:
    #   new_instances.should_receive(...)
    #   new_instances { |instance|  instance.should_receive(...) }
    #
    # new_instances is a short cut method for overriding the behavior of any
    # new instances created via a mocked class object.
    #
    # By default, new_instances will mock the behaviour of the :new
    # method.  If you wish to mock a different set of class methods,
    # just pass a list of symbols to as arguments.  (previous versions
    # also mocked :allocate by default.  If you need :allocate to be
    # mocked, just request it explicitly).
    #
    # For example, to stub only objects created by :make (and not
    # :new), use:
    #
    #    flexmock(ClassName).new_instances(:make).should_receive(...)
    #
    def new_instances(*allocators, &block)
      fail ArgumentError, "new_instances requires a Class to stub" unless
        Class === @obj
      location = caller
      allocators = [:initialize] if allocators.empty?

      expectation_recorder = ExpectationRecorder.new

      if allocators.delete(:initialize)
        initialize_stub(expectation_recorder, block)
      end

      allocators.each do |allocate_method|
        flexmock_define_expectation(location, allocate_method).and_return { |*args, **kw|
          create_new_mocked_object(
            allocate_method, args, kw, expectation_recorder, block)
        }
      end
      expectation_recorder
    end

    # Stubs the #initialize method on a class
    def initialize_stub(recorder, expectations_block)
      if !@initialize_override
        expectation_blocks    = @initialize_expectation_blocks = Array.new
        expectation_recorders = @initialize_expectation_recorders = Array.new
        @initialize_override = Module.new do
          define_method :initialize do |*args, **kw, &block|
            if self.class.respond_to?(:__flexmock_proxy) && (mock = self.class.__flexmock_proxy)
              container = mock.flexmock_container
              mock = container.flexmock(self)
              expectation_blocks.each do |b|
                b.call(mock)
              end
              expectation_recorders.each do |r|
                r.apply(mock)
              end
            end
            super(*args, **kw, &block)
          end
        end
        override = @initialize_override
        @obj.class_eval { prepend override }
      end
      if expectations_block
        @initialize_expectation_blocks    << expectations_block
      end
      @initialize_expectation_recorders << recorder
    end

    def initialize_stub?
      !!@initialize_override
    end

    def initialize_stub_remove
      if initialize_stub?
        @initialize_expectation_blocks.clear
        @initialize_expectation_recorders.clear
      end
    end

    # Create a new mocked object.
    #
    # The mocked object is created using the following steps:
    # (1) Allocate with the original allocation method (and args)
    # (2) Pass to the block for custom configuration.
    # (3) Apply any recorded expecations
    #
    def create_new_mocked_object(allocate_method, args, kw, recorder, block)
      new_obj = flexmock_invoke_original(allocate_method, args, kw, nil)
      mock = flexmock_container.flexmock(new_obj)
      block.call(mock) unless block.nil?
      recorder.apply(mock)
      new_obj
    end
    private :create_new_mocked_object

    # Invoke the original definition of method on the object supported by
    # the stub.
    def flexmock_invoke_original(method, args, kw, block)
      if (original_method = find_original_method(method))
        original_method.call(*args, **kw, &block)
      else
        @obj.__send__(:method_missing, method, *args, **kw, &block)
      end
    end

    # Verify that the mock has been properly called.  After verification,
    # detach the mocking infrastructure from the existing object.
    def flexmock_verify
      @mock.flexmock_verify
    end

    # Remove all traces of the mocking framework from the existing object.
    def flexmock_teardown
      if ! detached?
        initialize_stub_remove
        proxy_module_eval do
          methods = instance_methods(false).to_a
          methods.each do |m|
            remove_method m
          end
        end
        if @obj.instance_variable_defined?(:@flexmock_proxy) &&
            (box = @obj.instance_variable_get(:@flexmock_proxy))
          box.pop
        end
        @obj = nil
      end
    end

    # Forward to the mock's container.
    def flexmock_container
      @mock.flexmock_container
    end

    # Forward to the mock
    def flexmock_received?(*args, **kw)
      @mock.flexmock_received?(*args, **kw)
    end

    # Forward to the mock
    def flexmock_calls
      @mock.flexmock_calls
    end

    # Set the proxy's mock container.  This set value is ignored
    # because the proxy always uses the container of its mock.
    def flexmock_container=(container)
    end

    # Forward the request for the expectation director to the mock.
    def flexmock_expectations_for(method_name)
      @mock.flexmock_expectations_for(method_name)
    end

    # Forward the based on request.
    def flexmock_based_on(*args)
      @mock.flexmock_based_on(*args)
    end

    private

    # The singleton class of the object.
    def target_singleton_class
      @obj.singleton_class
    end

    # Evaluate a block (or string) in the context of the singleton
    # class of the target partial object.
    def target_class_eval(*args, **kw, &block)
      target_singleton_class.class_eval(*args, **kw, &block)
    end

    class ProxyDefinitionModule < Module
    end

    # Evaluate a block into the module we use to define the proxy methods
    def proxy_module_eval(*args, **kw, &block)
      if !@proxy_definition_module
        obj = @obj
        @proxy_definition_module = m = ProxyDefinitionModule.new do
          define_method("__flexmock_proxy") do
            if box = obj.instance_variable_get(:@flexmock_proxy)
              box.proxy
            end
          end
        end
        target_class_eval { prepend m }
      end
      @proxy_definition_module.class_eval(*args, **kw, &block)
    end

    # Hide the existing method definition with a singleton defintion
    # that proxies to our mock object.  If the current definition is a
    # singleton, we need to record the definition and remove it before
    # creating our own singleton method.  If the current definition is
    # not a singleton, all we need to do is override it with our own
    # singleton.
    def hide_existing_method(method_name)
      define_proxy_method(method_name)
    end

    # Define a proxy method that forwards to our mock object.  The
    # proxy method is defined as a singleton method on the object
    # being mocked.
    def define_proxy_method(method_name)
      if method_name =~ /=$/
        proxy_module_eval do
          define_method(method_name) do |*args, **kw, &block|
            __flexmock_proxy.mock.__send__(method_name, *args, **kw, &block)
          end
        end
      else
        proxy_module_eval <<-EOD
          def #{method_name}(*args, **kw, &block)
            FlexMock.verify_mocking_allowed!
            __flexmock_proxy.mock.#{method_name}(*args, **kw, &block)
          end
        EOD
      end
    end

    # Have we been detached from the existing object?
    def detached?
      @obj.nil?
    end
  end
end