minitest-mock 5.27.0.beta.1.beta.1

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/Manifest.txt ADDED
@@ -0,0 +1,7 @@
1
+ History.rdoc
2
+ Manifest.txt
3
+ README.rdoc
4
+ Rakefile
5
+ lib/minitest/mock.rb
6
+ test/minitest/metametameta.rb
7
+ test/minitest/test_minitest_mock.rb
data/README.rdoc ADDED
@@ -0,0 +1,139 @@
1
+ = minitest/mock
2
+
3
+ home :: https://minite.st/
4
+ code :: https://github.com/minitest/minitest-mock
5
+ bugs :: https://github.com/minitest/minitest-mock/issues
6
+ rdoc :: https://docs.seattlerb.org/minitest-mock
7
+ clog :: https://github.com/minitest/minitest-mock/blob/main/History.rdoc
8
+
9
+ == DESCRIPTION:
10
+
11
+ minitest/mock, by Steven Baker, is a beautifully tiny mock (and stub)
12
+ object framework.
13
+
14
+ The minitest-mock gem is an extraction of minitest/mock.rb from
15
+ minitest in order to make it easier to maintain independent of
16
+ minitest.
17
+
18
+ == FEATURES/PROBLEMS:
19
+
20
+ * minitest/mock - a simple and clean mock/stub system.
21
+ * Written by squishy human beings. Software can never be perfect. We will all eventually die.
22
+
23
+ == SYNOPSIS:
24
+
25
+ === Mocks
26
+
27
+ Mocks and stubs defined using terminology by Fowler & Meszaros at
28
+ https://www.martinfowler.com/bliki/TestDouble.html:
29
+
30
+ "Mocks are pre-programmed with expectations which form a specification
31
+ of the calls they are expected to receive. They can throw an exception
32
+ if they receive a call they don't expect and are checked during
33
+ verification to ensure they got all the calls they were expecting."
34
+
35
+ class MemeAsker
36
+ def initialize(meme)
37
+ @meme = meme
38
+ end
39
+
40
+ def ask(question)
41
+ method = question.tr(" ", "_") + "?"
42
+ @meme.__send__(method)
43
+ end
44
+ end
45
+
46
+ require "minitest/autorun"
47
+
48
+ describe MemeAsker, :ask do
49
+ describe "when passed an unpunctuated question" do
50
+ it "should invoke the appropriate predicate method on the meme" do
51
+ @meme = Minitest::Mock.new
52
+ @meme_asker = MemeAsker.new @meme
53
+ @meme.expect :will_it_blend?, :return_value
54
+
55
+ @meme_asker.ask "will it blend"
56
+
57
+ @meme.verify
58
+ end
59
+ end
60
+ end
61
+
62
+ ==== Multi-threading and Mocks
63
+
64
+ Minitest mocks do not support multi-threading. If it works, fine, if it doesn't
65
+ you can use regular ruby patterns and facilities like local variables. Here's
66
+ an example of asserting that code inside a thread is run:
67
+
68
+ def test_called_inside_thread
69
+ called = false
70
+ pr = Proc.new { called = true }
71
+ thread = Thread.new(&pr)
72
+ thread.join
73
+ assert called, "proc not called"
74
+ end
75
+
76
+ === Stubs
77
+
78
+ Mocks and stubs are defined using terminology by Fowler & Meszaros at
79
+ https://www.martinfowler.com/bliki/TestDouble.html:
80
+
81
+ "Stubs provide canned answers to calls made during the test".
82
+
83
+ Minitest's stub method overrides a single method for the duration of
84
+ the block.
85
+
86
+ def test_stale_eh
87
+ obj_under_test = Something.new
88
+
89
+ refute obj_under_test.stale?
90
+
91
+ Time.stub :now, Time.at(0) do # stub goes away once the block is done
92
+ assert obj_under_test.stale?
93
+ end
94
+ end
95
+
96
+ A note on stubbing: In order to stub a method, the method must
97
+ actually exist prior to stubbing. Use a singleton method to create a
98
+ new non-existing method:
99
+
100
+ def obj_under_test.fake_method
101
+ ...
102
+ end
103
+
104
+ == REQUIREMENTS:
105
+
106
+ * Ruby 3+. No magic is involved. I hope.
107
+
108
+ == INSTALL:
109
+
110
+ gem install minitest-mock
111
+
112
+ or
113
+
114
+ bundle add minitest-mock
115
+
116
+ == LICENSE:
117
+
118
+ (The MIT License)
119
+
120
+ Copyright (c) Ryan Davis, seattle.rb
121
+
122
+ Permission is hereby granted, free of charge, to any person obtaining
123
+ a copy of this software and associated documentation files (the
124
+ 'Software'), to deal in the Software without restriction, including
125
+ without limitation the rights to use, copy, modify, merge, publish,
126
+ distribute, sublicense, and/or sell copies of the Software, and to
127
+ permit persons to whom the Software is furnished to do so, subject to
128
+ the following conditions:
129
+
130
+ The above copyright notice and this permission notice shall be
131
+ included in all copies or substantial portions of the Software.
132
+
133
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
134
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
135
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
136
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
137
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
138
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
139
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ # -*- ruby -*-
2
+
3
+ require "hoe"
4
+ $:.unshift "lib" # to pick up lib/minitest/test_task.rb when minitest not installed
5
+
6
+ Hoe.plugin :minitest, :history, :email # seattlerb - perforce
7
+ Hoe.plugin :rdoc
8
+ Hoe.plugin :isolate
9
+ Hoe.plugin :halostatue
10
+
11
+ require "hoe/halostatue" # minor hack - I hate gemspec2's output
12
+ Hoe.plugins.delete :gemspec2
13
+
14
+ Hoe.spec "minitest-mock" do
15
+ developer "Ryan Davis", "ryand-ruby@zenspider.com"
16
+
17
+ license "MIT"
18
+
19
+ dependency "hoe-halostatue", "~> 2.1", :dev
20
+
21
+ require_ruby_version ">= 3.1"
22
+
23
+ self.checklist.clear
24
+ end
25
+
26
+ Minitest::TestTask.create :testW0 do |t|
27
+ t.warning = false
28
+ t.test_prelude = "$-w = nil"
29
+ end
30
+
31
+ # vim: syntax=Ruby
@@ -0,0 +1,329 @@
1
+ class MockExpectationError < StandardError; end # :nodoc:
2
+
3
+ module Minitest # :nodoc:
4
+
5
+ ##
6
+ # A simple and clean mock object framework.
7
+ #
8
+ # All mock objects are an instance of Mock
9
+
10
+ class Mock
11
+ VERSION = "5.27.0.beta.1" # :nodoc:
12
+
13
+ alias __respond_to? respond_to?
14
+
15
+ overridden_methods = %i[
16
+ ===
17
+ class
18
+ inspect
19
+ instance_eval
20
+ instance_variables
21
+ object_id
22
+ public_send
23
+ respond_to_missing?
24
+ send
25
+ to_s
26
+ ]
27
+
28
+ overridden_methods << :singleton_method_added if defined?(::DEBUGGER__)
29
+
30
+ instance_methods.each do |m|
31
+ undef_method m unless overridden_methods.include?(m) || m =~ /^__/
32
+ end
33
+
34
+ overridden_methods.map(&:to_sym).each do |method_id|
35
+ old_w, $-w = $-w, nil
36
+ define_method method_id do |*args, **kwargs, &b|
37
+ if @expected_calls.key? method_id then
38
+ method_missing(method_id, *args, **kwargs, &b)
39
+ else
40
+ super(*args, **kwargs, &b)
41
+ end
42
+ end
43
+ ensure
44
+ $-w = old_w
45
+ end
46
+
47
+ def initialize delegator = nil # :nodoc:
48
+ @delegator = delegator
49
+ @expected_calls = Hash.new { |calls, name| calls[name] = [] }
50
+ @actual_calls = Hash.new { |calls, name| calls[name] = [] }
51
+ end
52
+
53
+ @@KW_WARNED = false # :nodoc:
54
+
55
+ ##
56
+ # Expect that method +name+ is called, optionally with +args+ (and
57
+ # +kwargs+ or a +blk+), and returns +retval+.
58
+ #
59
+ # @mock.expect(:meaning_of_life, 42)
60
+ # @mock.meaning_of_life # => 42
61
+ #
62
+ # @mock.expect(:do_something_with, true, [some_obj, true])
63
+ # @mock.do_something_with(some_obj, true) # => true
64
+ #
65
+ # @mock.expect(:do_something_else, true) do |a1, a2|
66
+ # a1 == "buggs" && a2 == :bunny
67
+ # end
68
+ #
69
+ # +args+ is compared to the expected args using case equality (ie, the
70
+ # '===' operator), allowing for less specific expectations.
71
+ #
72
+ # @mock.expect(:uses_any_string, true, [String])
73
+ # @mock.uses_any_string("foo") # => true
74
+ # @mock.verify # => true
75
+ #
76
+ # @mock.expect(:uses_one_string, true, ["foo"])
77
+ # @mock.uses_one_string("bar") # => raises MockExpectationError
78
+ #
79
+ # If a method will be called multiple times, specify a new expect for each one.
80
+ # They will be used in the order you define them.
81
+ #
82
+ # @mock.expect(:ordinal_increment, 'first')
83
+ # @mock.expect(:ordinal_increment, 'second')
84
+ #
85
+ # @mock.ordinal_increment # => 'first'
86
+ # @mock.ordinal_increment # => 'second'
87
+ # @mock.ordinal_increment # => raises MockExpectationError "No more expects available for :ordinal_increment"
88
+ #
89
+
90
+ def expect name, retval, args = [], **kwargs, &blk
91
+ name = name.to_sym
92
+
93
+ if blk then
94
+ raise ArgumentError, "args ignored when block given" unless args.empty?
95
+ raise ArgumentError, "kwargs ignored when block given" unless kwargs.empty?
96
+ @expected_calls[name] << { :retval => retval, :block => blk }
97
+ else
98
+ raise ArgumentError, "args must be an array" unless Array === args
99
+
100
+ if ENV["MT_KWARGS_HAC\K"] && (Hash === args.last ||
101
+ Hash == args.last) then
102
+ if kwargs.empty? then
103
+ kwargs = args.pop
104
+ else
105
+ unless @@KW_WARNED then
106
+ from = caller(1..1).first
107
+ warn "Using MT_KWARGS_HAC\K yet passing kwargs. From #{from}"
108
+ @@KW_WARNED = true
109
+ end
110
+ end
111
+ end
112
+
113
+ @expected_calls[name] <<
114
+ { :retval => retval, :args => args, :kwargs => kwargs }
115
+ end
116
+ self
117
+ end
118
+
119
+ def __call name, data # :nodoc:
120
+ case data
121
+ when Hash then
122
+ args = data[:args].inspect[1..-2]
123
+ kwargs = data[:kwargs]
124
+ if kwargs && !kwargs.empty? then
125
+ args << ", " unless args.empty?
126
+ args << kwargs.inspect[1..-2]
127
+ end
128
+ "#{name}(#{args}) => #{data[:retval].inspect}"
129
+ else
130
+ data.map { |d| __call name, d }.join ", "
131
+ end
132
+ end
133
+
134
+ ##
135
+ # Verify that all methods were called as expected. Raises
136
+ # +MockExpectationError+ if the mock object was not called as
137
+ # expected.
138
+
139
+ def verify
140
+ @expected_calls.each do |name, expected|
141
+ actual = @actual_calls.fetch name, nil # defaults to []
142
+ raise MockExpectationError, "Expected #{__call name, expected[0]}" unless actual
143
+ raise MockExpectationError, "Expected #{__call name, expected[actual.size]}, got [#{__call name, actual}]" if
144
+ actual.size < expected.size
145
+ end
146
+ true
147
+ end
148
+
149
+ def method_missing sym, *args, **kwargs, &block # :nodoc:
150
+ unless @expected_calls.key? sym then
151
+ if @delegator && @delegator.respond_to?(sym)
152
+ return @delegator.public_send(sym, *args, **kwargs, &block)
153
+ else
154
+ raise NoMethodError, "unmocked method %p, expected one of %p" %
155
+ [sym, @expected_calls.keys.sort_by(&:to_s)]
156
+ end
157
+ end
158
+
159
+ index = @actual_calls[sym].length
160
+ expected_call = @expected_calls[sym][index]
161
+
162
+ unless expected_call then
163
+ raise MockExpectationError, "No more expects available for %p: %p %p" %
164
+ [sym, args, kwargs]
165
+ end
166
+
167
+ expected_args, expected_kwargs, retval, val_block =
168
+ expected_call.values_at :args, :kwargs, :retval, :block
169
+
170
+ expected_kwargs = kwargs.to_h { |ak, av| [ak, Object] } if
171
+ Hash == expected_kwargs
172
+
173
+ if val_block then
174
+ # keep "verify" happy
175
+ @actual_calls[sym] << expected_call
176
+
177
+ raise MockExpectationError, "mocked method %p failed block w/ %p %p" %
178
+ [sym, args, kwargs] unless val_block.call(*args, **kwargs, &block)
179
+
180
+ return retval
181
+ end
182
+
183
+ if expected_args.size != args.size then
184
+ raise ArgumentError, "mocked method %p expects %d arguments, got %p" %
185
+ [sym, expected_args.size, args]
186
+ end
187
+
188
+ if expected_kwargs.size != kwargs.size then
189
+ raise ArgumentError, "mocked method %p expects %d keyword arguments, got %p" %
190
+ [sym, expected_kwargs.size, kwargs]
191
+ end
192
+
193
+ zipped_args = expected_args.zip args
194
+ fully_matched = zipped_args.all? { |mod, a|
195
+ mod === a or mod == a
196
+ }
197
+
198
+ unless fully_matched then
199
+ fmt = "mocked method %p called with unexpected arguments %p"
200
+ raise MockExpectationError, fmt % [sym, args]
201
+ end
202
+
203
+ unless expected_kwargs.keys.sort == kwargs.keys.sort then
204
+ fmt = "mocked method %p called with unexpected keywords %p vs %p"
205
+ raise MockExpectationError, fmt % [sym, expected_kwargs.keys, kwargs.keys]
206
+ end
207
+
208
+ zipped_kwargs = expected_kwargs.to_h { |ek, ev|
209
+ av = kwargs[ek]
210
+ [ek, [ev, av]]
211
+ }
212
+
213
+ fully_matched = zipped_kwargs.all? { |ek, (ev, av)|
214
+ ev === av or ev == av
215
+ }
216
+
217
+ unless fully_matched then
218
+ fmt = "mocked method %p called with unexpected keyword arguments %p vs %p"
219
+ raise MockExpectationError, fmt % [sym, expected_kwargs, kwargs]
220
+ end
221
+
222
+ @actual_calls[sym] << {
223
+ :retval => retval,
224
+ :args => zipped_args.map { |e, a| e === a ? e : a },
225
+ :kwargs => zipped_kwargs.to_h { |k, (e, a)| [k, e === a ? e : a] },
226
+ }
227
+
228
+ retval
229
+ end
230
+
231
+ def respond_to? sym, include_private = false # :nodoc:
232
+ return true if @expected_calls.key? sym.to_sym
233
+ return true if @delegator && @delegator.respond_to?(sym, include_private)
234
+ __respond_to? sym, include_private
235
+ end
236
+ end
237
+ end
238
+
239
+ module Minitest::Assertions
240
+ ##
241
+ # Assert that the mock verifies correctly and fail if not.
242
+
243
+ def assert_mock mock, msg = nil
244
+ assert mock.verify
245
+ rescue MockExpectationError => e
246
+ msg = message(msg) { e.message }
247
+ flunk msg
248
+ end
249
+ end
250
+
251
+ module Minitest::Expectations
252
+ ##
253
+ # See Minitest::Assertions#assert_mock.
254
+ #
255
+ # _(collection).must_verify
256
+ #
257
+ # :method: must_verify
258
+
259
+ infect_an_assertion :assert_mock, :must_verify, :unary if
260
+ defined?(infect_an_assertion)
261
+ end
262
+
263
+ ##
264
+ # Object extensions for Minitest::Mock.
265
+
266
+ class Object
267
+
268
+ ##
269
+ # Add a temporary stubbed method replacing +name+ for the duration
270
+ # of the +block+. If +val_or_callable+ responds to #call, then it
271
+ # returns the result of calling it, otherwise returns the value
272
+ # as-is. If stubbed method yields a block, +block_args+ will be
273
+ # passed along. Cleans up the stub at the end of the +block+. The
274
+ # method +name+ must exist before stubbing.
275
+ #
276
+ # def test_stale_eh
277
+ # obj_under_test = Something.new
278
+ # refute obj_under_test.stale?
279
+ #
280
+ # Time.stub :now, Time.at(0) do
281
+ # assert obj_under_test.stale?
282
+ # end
283
+ # end
284
+ #--
285
+ # NOTE: keyword args in callables are NOT checked for correctness
286
+ # against the existing method. Too many edge cases to be worth it.
287
+
288
+ def stub name, val_or_callable, *block_args, **block_kwargs, &block
289
+ new_name = "__minitest_stub__#{name}"
290
+
291
+ metaclass = class << self; self; end
292
+
293
+ if respond_to? name and not methods.map(&:to_s).include? name.to_s then
294
+ metaclass.send :define_method, name do |*args, **kwargs|
295
+ super(*args, **kwargs)
296
+ end
297
+ end
298
+
299
+ metaclass.send :alias_method, new_name, name
300
+
301
+ if ENV["MT_KWARGS_HAC\K"] then
302
+ metaclass.send :define_method, name do |*args, &blk|
303
+ if val_or_callable.respond_to? :call then
304
+ val_or_callable.call(*args, &blk)
305
+ else
306
+ blk.call(*block_args, **block_kwargs) if blk
307
+ val_or_callable
308
+ end
309
+ end
310
+ else
311
+ metaclass.send :define_method, name do |*args, **kwargs, &blk|
312
+ if val_or_callable.respond_to? :call then
313
+ val_or_callable.call(*args, **kwargs, &blk)
314
+ else
315
+ if blk then
316
+ blk.call(*block_args, **block_kwargs)
317
+ end
318
+ val_or_callable
319
+ end
320
+ end
321
+ end
322
+
323
+ block[self]
324
+ ensure
325
+ metaclass.send :undef_method, name
326
+ metaclass.send :alias_method, name, new_name
327
+ metaclass.send :undef_method, new_name
328
+ end
329
+ end
@@ -0,0 +1,150 @@
1
+ require "tempfile"
2
+ require "stringio"
3
+ require "minitest/autorun"
4
+
5
+ class Minitest::Test
6
+ def with_empty_backtrace_filter
7
+ with_backtrace_filter Minitest::BacktraceFilter.new %r%.% do
8
+ yield
9
+ end
10
+ end
11
+
12
+ def with_backtrace_filter filter
13
+ original = Minitest.backtrace_filter
14
+
15
+ Minitest::Test.io_lock.synchronize do # try not to trounce in parallel
16
+ begin
17
+ Minitest.backtrace_filter = filter
18
+ yield
19
+ ensure
20
+ Minitest.backtrace_filter = original
21
+ end
22
+ end
23
+ end
24
+
25
+ def error_on_warn?
26
+ defined?(Minitest::ErrorOnWarning)
27
+ end
28
+
29
+ def assert_deprecation re = /DEPRECATED/
30
+ re = // if $-w.nil? # "skip" if running `rake testW0`
31
+ assert_output "", re do
32
+ yield
33
+ end
34
+ rescue Minitest::UnexpectedWarning => e # raised if -Werror was used
35
+ assert_match re, e.message
36
+ end
37
+ end
38
+
39
+ class FakeNamedTest < Minitest::Test
40
+ @@count = 0
41
+
42
+ def self.name
43
+ @fake_name ||= begin
44
+ @@count += 1
45
+ "FakeNamedTest%02d" % @@count
46
+ end
47
+ end
48
+ end
49
+
50
+ module MyModule; end
51
+ class AnError < StandardError; include MyModule; end
52
+
53
+ class MetaMetaMetaTestCase < Minitest::Test
54
+ attr_accessor :reporter, :output, :tu
55
+
56
+ def with_stderr err
57
+ old = $stderr
58
+ $stderr = err
59
+ yield
60
+ ensure
61
+ $stderr = old
62
+ end
63
+
64
+ def run_tu_with_fresh_reporter flags = %w[--seed 42]
65
+ options = Minitest.process_args flags
66
+
67
+ @output = StringIO.new(+"")
68
+
69
+ self.reporter = Minitest::CompositeReporter.new
70
+ reporter << Minitest::SummaryReporter.new(@output, options)
71
+ reporter << Minitest::ProgressReporter.new(@output, options)
72
+
73
+ with_stderr @output do
74
+ reporter.start
75
+
76
+ yield reporter if block_given?
77
+
78
+ @tus ||= [@tu]
79
+ @tus.each do |tu|
80
+ Minitest::Runnable.runnables.delete tu
81
+
82
+ tu.run reporter, options
83
+ end
84
+
85
+ reporter.report
86
+ end
87
+ end
88
+
89
+ def first_reporter
90
+ reporter.reporters.first
91
+ end
92
+
93
+ def assert_report expected, flags = %w[--seed 42], &block
94
+ header = <<~EOM
95
+ Run options: #{flags.map { |s| s.include?("|") ? s.inspect : s }.join " "}
96
+
97
+ # Running:
98
+
99
+ EOM
100
+
101
+ run_tu_with_fresh_reporter flags, &block
102
+
103
+ output = normalize_output @output.string.dup
104
+
105
+ assert_equal header + expected, output
106
+ end
107
+
108
+ def normalize_output output
109
+ output.sub!(/Finished in .*/, "Finished in 0.00")
110
+ output.sub!(/Loaded suite .*/, "Loaded suite blah")
111
+
112
+ output.gsub!(/FakeNamedTest\d+/, "FakeNamedTestXX")
113
+ output.gsub!(/ = \d+.\d\d s = /, " = 0.00 s = ")
114
+ output.gsub!(/0x[A-Fa-f0-9]+/, "0xXXX")
115
+ output.gsub!(/ +$/, "")
116
+
117
+ file = ->(s) { s.start_with?("/") ? "FULLFILE" : "FILE" }
118
+
119
+ if windows? then
120
+ output.gsub!(/\[(?:[A-Za-z]:)?[^\]:]+:\d+\]/, "[FILE:LINE]")
121
+ output.gsub!(/^(\s+)(?:[A-Za-z]:)?[^:]+:\d+:in [`']/, '\1FILE:LINE:in \'')
122
+ else
123
+ output.gsub!(/\[([^\]:]+):\d+\]/) { "[#{file[$1]}:LINE]" }
124
+ output.gsub!(/^(\s+)([^:]+):\d+:in [`']/) { "#{$1}#{file[$2]}:LINE:in '" }
125
+ end
126
+
127
+ output.gsub!(/in [`']block in (?:([^']+)[#.])?/, "in 'block in")
128
+ output.gsub!(/in [`'](?:([^']+)[#.])?/, "in '")
129
+
130
+ output.gsub!(/( at )([^:]+):\d+/) { "#{$1}[#{file[$2]}:LINE]" } # eval?
131
+
132
+ output
133
+ end
134
+
135
+ def restore_env
136
+ old_value = ENV["MT_NO_SKIP_MSG"]
137
+ ENV.delete "MT_NO_SKIP_MSG"
138
+
139
+ yield
140
+ ensure
141
+ ENV["MT_NO_SKIP_MSG"] = old_value
142
+ end
143
+
144
+ def setup
145
+ super
146
+ Minitest.seed = 42
147
+ Minitest::Test.reset
148
+ @tu = nil
149
+ end
150
+ end