Class: Minitest::Mock

Inherits:
Object show all
Defined in:
lib/minitest/mock.rb

Overview

A simple and clean mock object framework.

All mock objects are an instance of Mock

Constant Summary collapse

@@KW_WARNED =

:nodoc:

false

Instance Method Summary collapse

Constructor Details

#initialize(delegator = nil) ⇒ Mock

:nodoc:



53
54
55
56
57
# File 'lib/minitest/mock.rb', line 53

def initialize delegator = nil # :nodoc:
  @delegator = delegator
  @expected_calls = Hash.new { |calls, name| calls[name] = [] }
  @actual_calls   = Hash.new { |calls, name| calls[name] = [] }
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(sym, *args, **kwargs, &block) ⇒ Object

:nodoc:



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
# File 'lib/minitest/mock.rb', line 155

def method_missing sym, *args, **kwargs, &block # :nodoc:
  unless @expected_calls.key? sym then
    if @delegator && @delegator.respond_to?(sym)
      if kwargs.empty? then # FIX: drop this after 2.7 dead
        return @delegator.public_send(sym, *args, &block)
      else
        return @delegator.public_send(sym, *args, **kwargs, &block)
      end
    else
      raise NoMethodError, "unmocked method %p, expected one of %p" %
        [sym, @expected_calls.keys.sort_by(&:to_s)]
    end
  end

  index = @actual_calls[sym].length
  expected_call = @expected_calls[sym][index]

  unless expected_call then
    raise MockExpectationError, "No more expects available for %p: %p %p" %
      [sym, args, kwargs]
  end

  expected_args, expected_kwargs, retval, val_block =
    expected_call.values_at :args, :kwargs, :retval, :block

  expected_kwargs = kwargs.to_h { |ak, av| [ak, Object] } if
    Hash == expected_kwargs

  if val_block then
    # keep "verify" happy
    @actual_calls[sym] << expected_call

    raise MockExpectationError, "mocked method %p failed block w/ %p %p" %
      [sym, args, kwargs] unless val_block.call(*args, **kwargs, &block)

    return retval
  end

  if expected_args.size != args.size then
    raise ArgumentError, "mocked method %p expects %d arguments, got %p" %
      [sym, expected_args.size, args]
  end

  if expected_kwargs.size != kwargs.size then
    raise ArgumentError, "mocked method %p expects %d keyword arguments, got %p" %
      [sym, expected_kwargs.size, kwargs]
  end

  zipped_args = expected_args.zip args
  fully_matched = zipped_args.all? { |mod, a|
    mod === a or mod == a
  }

  unless fully_matched then
    fmt = "mocked method %p called with unexpected arguments %p"
    raise MockExpectationError, fmt % [sym, args]
  end

  unless expected_kwargs.keys.sort == kwargs.keys.sort then
    fmt = "mocked method %p called with unexpected keywords %p vs %p"
    raise MockExpectationError, fmt % [sym, expected_kwargs.keys, kwargs.keys]
  end

  zipped_kwargs = expected_kwargs.to_h { |ek, ev|
    av = kwargs[ek]
    [ek, [ev, av]]
  }

  fully_matched = zipped_kwargs.all? { |ek, (ev, av)|
    ev === av or ev == av
  }

  unless fully_matched then
    fmt = "mocked method %p called with unexpected keyword arguments %p vs %p"
    raise MockExpectationError, fmt % [sym, expected_kwargs, kwargs]
  end

  @actual_calls[sym] << {
    :retval => retval,
    :args   => zipped_args.map { |e, a| e === a ? e : a },
    :kwargs => zipped_kwargs.to_h { |k, (e, a)| [k, e === a ? e : a] },
  }

  retval
end

Instance Method Details

#__call(name, data) ⇒ Object

:nodoc:



125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/minitest/mock.rb', line 125

def __call name, data # :nodoc:
  case data
  when Hash then
    args   = data[:args].inspect[1..-2]
    kwargs = data[:kwargs]
    if kwargs && !kwargs.empty? then
      args << ", " unless args.empty?
      args << kwargs.inspect[1..-2]
    end
    "#{name}(#{args}) => #{data[:retval].inspect}"
  else
    data.map { |d| __call name, d }.join ", "
  end
end

#__respond_to?Object



11
# File 'lib/minitest/mock.rb', line 11

alias __respond_to? respond_to?

#expect(name, retval, args = [], **kwargs, &blk) ⇒ Object

Expect that method name is called, optionally with args (and kwargs or a blk), and returns retval.

@mock.expect(:meaning_of_life, 42)
@mock.meaning_of_life # => 42

@mock.expect(:do_something_with, true, [some_obj, true])
@mock.do_something_with(some_obj, true) # => true

@mock.expect(:do_something_else, true) do |a1, a2|
  a1 == "buggs" && a2 == :bunny
end

args is compared to the expected args using case equality (ie, the ‘===’ operator), allowing for less specific expectations.

@mock.expect(:uses_any_string, true, [String])
@mock.uses_any_string("foo") # => true
@mock.verify  # => true

@mock.expect(:uses_one_string, true, ["foo"])
@mock.uses_one_string("bar") # => raises MockExpectationError

If a method will be called multiple times, specify a new expect for each one. They will be used in the order you define them.

@mock.expect(:ordinal_increment, 'first')
@mock.expect(:ordinal_increment, 'second')

@mock.ordinal_increment # => 'first'
@mock.ordinal_increment # => 'second'
@mock.ordinal_increment # => raises MockExpectationError "No more expects available for :ordinal_increment"


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
# File 'lib/minitest/mock.rb', line 96

def expect name, retval, args = [], **kwargs, &blk
  name = name.to_sym

  if blk then
    raise ArgumentError, "args ignored when block given" unless args.empty?
    raise ArgumentError, "kwargs ignored when block given" unless kwargs.empty?
    @expected_calls[name] << { :retval => retval, :block => blk }
  else
    raise ArgumentError, "args must be an array" unless Array === args

    if ENV["MT_KWARGS_HAC\K"] && (Hash === args.last ||
                                  Hash ==  args.last) then
      if kwargs.empty? then
        kwargs = args.pop
      else
        unless @@KW_WARNED then
          from = caller(1..1).first
          warn "Using MT_KWARGS_HAC\K yet passing kwargs. From #{from}"
          @@KW_WARNED = true
        end
      end
    end

    @expected_calls[name] <<
      { :retval => retval, :args => args, :kwargs => kwargs }
  end
  self
end

#respond_to?(sym, include_private = false) ⇒ Boolean

:nodoc:

Returns:

  • (Boolean)


241
242
243
244
245
# File 'lib/minitest/mock.rb', line 241

def respond_to? sym, include_private = false # :nodoc:
  return true if @expected_calls.key? sym.to_sym
  return true if @delegator && @delegator.respond_to?(sym, include_private)
  __respond_to? sym, include_private
end

#verifyObject

Verify that all methods were called as expected. Raises MockExpectationError if the mock object was not called as expected.



145
146
147
148
149
150
151
152
153
# File 'lib/minitest/mock.rb', line 145

def verify
  @expected_calls.each do |name, expected|
    actual = @actual_calls.fetch name, nil # defaults to []
    raise MockExpectationError, "Expected #{__call name, expected[0]}" unless actual
    raise MockExpectationError, "Expected #{__call name, expected[actual.size]}, got [#{__call name, actual}]" if
      actual.size < expected.size
  end
  true
end