rspec-mocks 2.5.0 → 2.6.0.rc2
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/.travis.yml +7 -0
- data/Gemfile +13 -12
- data/Guardfile +3 -3
- data/README.md +2 -0
- data/features/.nav +4 -0
- data/features/Changelog.md +12 -0
- data/features/README.markdown +44 -4
- data/features/message_expectations/any_instance.feature +19 -0
- data/features/message_expectations/block_local_expectations.feature.pending +2 -2
- data/features/message_expectations/expect_message.feature +3 -3
- data/features/message_expectations/warn_when_expectation_is_set_on_nil.feature +3 -3
- data/features/method_stubs/README.md +32 -24
- data/features/method_stubs/any_instance.feature +23 -0
- data/features/method_stubs/as_null_object.feature +36 -0
- data/features/method_stubs/simple_return_value.feature +2 -2
- data/features/method_stubs/stub_chain.feature +13 -6
- data/features/method_stubs/stub_implementation.feature +1 -1
- data/features/method_stubs/to_ary.feature +45 -0
- data/features/outside_rspec/configuration.feature +3 -3
- data/features/outside_rspec/standalone.feature +2 -2
- data/features/step_definitions/additional_cli_steps.rb +4 -0
- data/features/support/env.rb +5 -1
- data/lib/rspec/mocks.rb +2 -1
- data/lib/rspec/mocks/any_instance.rb +246 -0
- data/lib/rspec/mocks/extensions/psych.rb +23 -0
- data/lib/rspec/mocks/framework.rb +1 -0
- data/lib/rspec/mocks/message_expectation.rb +7 -4
- data/lib/rspec/mocks/methods.rb +1 -1
- data/lib/rspec/mocks/mock.rb +2 -2
- data/lib/rspec/mocks/serialization.rb +5 -3
- data/lib/rspec/mocks/space.rb +1 -1
- data/lib/rspec/mocks/version.rb +1 -1
- data/spec/rspec/mocks/and_yield_spec.rb +3 -3
- data/spec/rspec/mocks/any_instance_spec.rb +578 -0
- data/spec/rspec/mocks/any_number_of_times_spec.rb +22 -28
- data/spec/rspec/mocks/at_least_spec.rb +6 -0
- data/spec/rspec/mocks/at_most_spec.rb +6 -0
- data/spec/rspec/mocks/bug_report_10263_spec.rb +12 -14
- data/spec/rspec/mocks/bug_report_7611_spec.rb +2 -4
- data/spec/rspec/mocks/failing_argument_matchers_spec.rb +45 -46
- data/spec/rspec/mocks/nil_expectation_warning_spec.rb +1 -2
- data/spec/rspec/mocks/passing_argument_matchers_spec.rb +119 -122
- data/spec/rspec/mocks/precise_counts_spec.rb +6 -0
- data/spec/rspec/mocks/serialization_spec.rb +21 -3
- data/spec/rspec/mocks/stub_chain_spec.rb +72 -37
- data/spec/rspec/mocks/stub_spec.rb +64 -61
- data/spec/rspec/mocks/to_ary_spec.rb +31 -0
- metadata +32 -15
- data/spec/rspec/mocks/bug_report_7805_spec.rb +0 -22
- data/spec/rspec/mocks/bug_report_8302_spec.rb +0 -26
@@ -0,0 +1,45 @@
|
|
1
|
+
Feature: double handling to_ary
|
2
|
+
|
3
|
+
Ruby implicitly sends to_ary to any objects in an Array when the array
|
4
|
+
receives `flatten`:
|
5
|
+
|
6
|
+
[obj].flatten # => obj.to_ary
|
7
|
+
|
8
|
+
To support this, an RSpec double will raise a NoMethodError when it receives
|
9
|
+
`to_ary`, unless that method is explicitly stubbed.
|
10
|
+
|
11
|
+
Scenario: double receiving to_ary
|
12
|
+
Given a file named "example.rb" with:
|
13
|
+
"""
|
14
|
+
describe "a double receiving to_ary" do
|
15
|
+
shared_examples "to_ary" do
|
16
|
+
it "returns nil" do
|
17
|
+
expect do
|
18
|
+
obj.to_ary.should be_nil
|
19
|
+
end.to raise_error(NoMethodError)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "can be overridden with a stub" do
|
23
|
+
obj.stub(:to_ary) { :non_nil_value }
|
24
|
+
obj.to_ary.should be(:non_nil_value)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "supports Array#flatten" do
|
28
|
+
obj = double('foo')
|
29
|
+
[obj].flatten.should eq([obj])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context "double as_null_object" do
|
34
|
+
let(:obj) { double('obj').as_null_object }
|
35
|
+
include_examples "to_ary"
|
36
|
+
end
|
37
|
+
|
38
|
+
context "double without as_null_object" do
|
39
|
+
let(:obj) { double('obj') }
|
40
|
+
include_examples "to_ary"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
"""
|
44
|
+
When I run `rspec example.rb`
|
45
|
+
Then the examples should all pass
|
@@ -42,7 +42,7 @@ Feature: configure any test framework to use rspec-mocks
|
|
42
42
|
puts example.respond_to?(:stub)
|
43
43
|
"""
|
44
44
|
|
45
|
-
When I run
|
45
|
+
When I run `ruby foo.rb`
|
46
46
|
Then the output should contain "true"
|
47
47
|
But the output should not contain "false"
|
48
48
|
|
@@ -60,7 +60,7 @@ Feature: configure any test framework to use rspec-mocks
|
|
60
60
|
puts obj.respond_to?(:stub)
|
61
61
|
"""
|
62
62
|
|
63
|
-
When I run
|
63
|
+
When I run `ruby foo.rb`
|
64
64
|
Then the output should contain "true"
|
65
65
|
But the output should not contain "false"
|
66
66
|
|
@@ -76,7 +76,7 @@ Feature: configure any test framework to use rspec-mocks
|
|
76
76
|
puts obj.respond_to?(:stub)
|
77
77
|
"""
|
78
78
|
|
79
|
-
When I run
|
79
|
+
When I run `ruby foo.rb`
|
80
80
|
Then the output should contain "false"
|
81
81
|
But the output should not contain "true"
|
82
82
|
|
@@ -13,7 +13,7 @@ Feature: standalone
|
|
13
13
|
greeter.stub(:say_hi) { "Hello!" }
|
14
14
|
puts greeter.say_hi
|
15
15
|
"""
|
16
|
-
When I run
|
16
|
+
When I run `ruby example.rb`
|
17
17
|
Then the output should contain "Hello!"
|
18
18
|
|
19
19
|
Scenario: message expectation outside rspec
|
@@ -26,7 +26,7 @@ Feature: standalone
|
|
26
26
|
|
27
27
|
RSpec::Mocks.verify
|
28
28
|
"""
|
29
|
-
When I run
|
29
|
+
When I run `ruby example.rb`
|
30
30
|
Then the output should contain "say_hi(any args) (RSpec::Mocks::MockExpectationError)"
|
31
31
|
Then the output should contain "expected: 1 time"
|
32
32
|
Then the output should contain "received: 0 times"
|
data/features/support/env.rb
CHANGED
data/lib/rspec/mocks.rb
CHANGED
@@ -177,7 +177,8 @@ module RSpec
|
|
177
177
|
attr_accessor :space
|
178
178
|
|
179
179
|
def setup(includer)
|
180
|
-
Object.class_eval { include RSpec::Mocks::Methods }
|
180
|
+
Object.class_eval { include RSpec::Mocks::Methods }
|
181
|
+
Class.class_eval { include RSpec::Mocks::AnyInstance }
|
181
182
|
(class << includer; self; end).class_eval do
|
182
183
|
include RSpec::Mocks::ExampleMethods
|
183
184
|
end
|
@@ -0,0 +1,246 @@
|
|
1
|
+
module RSpec
|
2
|
+
module Mocks
|
3
|
+
module AnyInstance
|
4
|
+
class Chain
|
5
|
+
[
|
6
|
+
:with, :and_return, :and_raise, :and_yield,
|
7
|
+
:once, :twice, :any_number_of_times,
|
8
|
+
:exactly, :times, :never,
|
9
|
+
:at_least, :at_most
|
10
|
+
].each do |method_name|
|
11
|
+
class_eval(<<-EOM, __FILE__, __LINE__)
|
12
|
+
def #{method_name}(*args, &block)
|
13
|
+
record(:#{method_name}, *args, &block)
|
14
|
+
end
|
15
|
+
EOM
|
16
|
+
end
|
17
|
+
|
18
|
+
def playback!(instance)
|
19
|
+
messages.inject(instance) do |instance, message|
|
20
|
+
instance.send(*message.first, &message.last)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def constrained_to_any_of?(*constraints)
|
25
|
+
constraints.any? do |constraint|
|
26
|
+
messages.any? do |message|
|
27
|
+
message.first.first == constraint
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
def messages
|
34
|
+
@messages ||= []
|
35
|
+
end
|
36
|
+
|
37
|
+
def last_message
|
38
|
+
messages.last.first.first unless messages.empty?
|
39
|
+
end
|
40
|
+
|
41
|
+
def record(rspec_method_name, *args, &block)
|
42
|
+
verify_invocation_order(rspec_method_name, *args, &block)
|
43
|
+
messages << [args.unshift(rspec_method_name), block]
|
44
|
+
self
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class StubChain < Chain
|
49
|
+
def initialize(*args, &block)
|
50
|
+
record(:stub, *args, &block)
|
51
|
+
end
|
52
|
+
|
53
|
+
def invocation_order
|
54
|
+
@invocation_order ||= {
|
55
|
+
:stub => [nil],
|
56
|
+
:with => [:stub],
|
57
|
+
:and_return => [:with, :stub],
|
58
|
+
:and_raise => [:with, :stub],
|
59
|
+
:and_yield => [:with, :stub]
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
def expectation_filfilled?
|
64
|
+
true
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
def verify_invocation_order(rspec_method_name, *args, &block)
|
69
|
+
unless invocation_order[rspec_method_name].include?(last_message)
|
70
|
+
raise(NoMethodError, "Undefined method #{rspec_method_name}")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class ExpectationChain < Chain
|
76
|
+
def initialize(*args, &block)
|
77
|
+
record(:should_receive, *args, &block)
|
78
|
+
@expectation_fulfilled = false
|
79
|
+
end
|
80
|
+
|
81
|
+
def invocation_order
|
82
|
+
@invocation_order ||= {
|
83
|
+
:should_receive => [nil],
|
84
|
+
:with => [:should_receive],
|
85
|
+
:and_return => [:with, :should_receive],
|
86
|
+
:and_raise => [:with, :should_receive]
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
def expectation_fulfilled!
|
91
|
+
@expectation_fulfilled = true
|
92
|
+
end
|
93
|
+
|
94
|
+
def expectation_filfilled?
|
95
|
+
@expectation_fulfilled || constrained_to_any_of?(:never, :any_number_of_times)
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
def verify_invocation_order(rspec_method_name, *args, &block)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class Recorder
|
104
|
+
def initialize(klass)
|
105
|
+
@message_chains = {}
|
106
|
+
@observed_methods = []
|
107
|
+
@played_methods = {}
|
108
|
+
@klass = klass
|
109
|
+
@expectation_set = false
|
110
|
+
end
|
111
|
+
|
112
|
+
def stub(method_name, *args, &block)
|
113
|
+
observe!(method_name)
|
114
|
+
@message_chains[method_name] = StubChain.new(method_name, *args, &block)
|
115
|
+
end
|
116
|
+
|
117
|
+
def should_receive(method_name, *args, &block)
|
118
|
+
observe!(method_name)
|
119
|
+
@expectation_set = true
|
120
|
+
@message_chains[method_name] = ExpectationChain.new(method_name, *args, &block)
|
121
|
+
end
|
122
|
+
|
123
|
+
def stop_observing!
|
124
|
+
@observed_methods.each do |method_name|
|
125
|
+
restore_method!(method_name)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def playback!(instance, method_name)
|
130
|
+
RSpec::Mocks::space.add(instance)
|
131
|
+
@message_chains[method_name].playback!(instance)
|
132
|
+
@played_methods[method_name] = instance
|
133
|
+
received_expected_message!(method_name) if has_expectation?(method_name)
|
134
|
+
end
|
135
|
+
|
136
|
+
def instance_that_received(method_name)
|
137
|
+
@played_methods[method_name]
|
138
|
+
end
|
139
|
+
|
140
|
+
def verify
|
141
|
+
if @expectation_set && !each_expectation_filfilled?
|
142
|
+
raise RSpec::Mocks::MockExpectationError, "Exactly one instance should have received the following message(s) but didn't: #{unfulfilled_expectations.sort.join(', ')}"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
def each_expectation_filfilled?
|
148
|
+
@message_chains.all? do |method_name, chain|
|
149
|
+
chain.expectation_filfilled?
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def has_expectation?(method_name)
|
154
|
+
@message_chains[method_name].is_a?(ExpectationChain)
|
155
|
+
end
|
156
|
+
|
157
|
+
def unfulfilled_expectations
|
158
|
+
@message_chains.map do |method_name, chain|
|
159
|
+
method_name.to_s if chain.is_a?(ExpectationChain) unless chain.expectation_filfilled?
|
160
|
+
end.compact
|
161
|
+
end
|
162
|
+
|
163
|
+
def received_expected_message!(method_name)
|
164
|
+
@message_chains[method_name].expectation_fulfilled!
|
165
|
+
restore_method!(method_name)
|
166
|
+
mark_invoked!(method_name)
|
167
|
+
end
|
168
|
+
|
169
|
+
def restore_method!(method_name)
|
170
|
+
if @klass.method_defined?(build_alias_method_name(method_name))
|
171
|
+
restore_original_method!(method_name)
|
172
|
+
else
|
173
|
+
remove_dummy_method!(method_name)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def build_alias_method_name(method_name)
|
178
|
+
"__#{method_name}_without_any_instance__"
|
179
|
+
end
|
180
|
+
|
181
|
+
def restore_original_method!(method_name)
|
182
|
+
alias_method_name = build_alias_method_name(method_name)
|
183
|
+
@klass.class_eval do
|
184
|
+
alias_method method_name, alias_method_name
|
185
|
+
remove_method alias_method_name
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def remove_dummy_method!(method_name)
|
190
|
+
@klass.class_eval do
|
191
|
+
remove_method method_name
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def backup_method!(method_name)
|
196
|
+
alias_method_name = build_alias_method_name(method_name)
|
197
|
+
@klass.class_eval do
|
198
|
+
if method_defined?(method_name)
|
199
|
+
alias_method alias_method_name, method_name
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def observe!(method_name)
|
205
|
+
@observed_methods << method_name
|
206
|
+
backup_method!(method_name)
|
207
|
+
@klass.class_eval(<<-EOM, __FILE__, __LINE__)
|
208
|
+
def #{method_name}(*args, &blk)
|
209
|
+
self.class.__recorder.playback!(self, :#{method_name})
|
210
|
+
self.send(:#{method_name}, *args, &blk)
|
211
|
+
end
|
212
|
+
EOM
|
213
|
+
end
|
214
|
+
|
215
|
+
def mark_invoked!(method_name)
|
216
|
+
backup_method!(method_name)
|
217
|
+
@klass.class_eval(<<-EOM, __FILE__, __LINE__)
|
218
|
+
def #{method_name}(*args, &blk)
|
219
|
+
method_name = :#{method_name}
|
220
|
+
current_instance = self
|
221
|
+
invoked_instance = self.class.__recorder.instance_that_received(method_name)
|
222
|
+
raise RSpec::Mocks::MockExpectationError, "The message '#{method_name}' was received by \#{self.inspect} but has already been received by \#{invoked_instance}"
|
223
|
+
end
|
224
|
+
EOM
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def any_instance
|
229
|
+
RSpec::Mocks::space.add(self)
|
230
|
+
__recorder
|
231
|
+
end
|
232
|
+
|
233
|
+
def rspec_verify
|
234
|
+
__recorder.verify
|
235
|
+
super
|
236
|
+
ensure
|
237
|
+
__recorder.stop_observing!
|
238
|
+
@__recorder = nil
|
239
|
+
end
|
240
|
+
|
241
|
+
def __recorder
|
242
|
+
@__recorder ||= AnyInstance::Recorder.new(self)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
if defined?(Psych) && Psych.respond_to?(:dump)
|
2
|
+
module Psych
|
3
|
+
class << self
|
4
|
+
def dump_with_mocks(object, *args)
|
5
|
+
return dump_without_mocks(object, *args) unless object.instance_variable_defined?(:@mock_proxy)
|
6
|
+
|
7
|
+
mp = object.instance_variable_get(:@mock_proxy)
|
8
|
+
return dump_without_mocks(object, *args) unless mp.is_a?(::RSpec::Mocks::Proxy)
|
9
|
+
|
10
|
+
object.send(:remove_instance_variable, :@mock_proxy)
|
11
|
+
|
12
|
+
begin
|
13
|
+
dump_without_mocks(object, *args)
|
14
|
+
ensure
|
15
|
+
object.instance_variable_set(:@mock_proxy,mp)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
alias_method :dump_without_mocks, :dump
|
20
|
+
alias_method :dump, :dump_with_mocks
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -249,22 +249,25 @@ module RSpec
|
|
249
249
|
end
|
250
250
|
|
251
251
|
def with(*args, &block)
|
252
|
-
@return_block = block if block_given?
|
252
|
+
@return_block = block if block_given? unless args.empty?
|
253
253
|
@args_expectation = ArgumentExpectation.new(*args, &block)
|
254
254
|
self
|
255
255
|
end
|
256
256
|
|
257
|
-
def exactly(n)
|
257
|
+
def exactly(n, &block)
|
258
|
+
@method_block = block if block
|
258
259
|
set_expected_received_count :exactly, n
|
259
260
|
self
|
260
261
|
end
|
261
262
|
|
262
|
-
def at_least(n)
|
263
|
+
def at_least(n, &block)
|
264
|
+
@method_block = block if block
|
263
265
|
set_expected_received_count :at_least, n
|
264
266
|
self
|
265
267
|
end
|
266
268
|
|
267
|
-
def at_most(n)
|
269
|
+
def at_most(n, &block)
|
270
|
+
@method_block = block if block
|
268
271
|
set_expected_received_count :at_most, n
|
269
272
|
self
|
270
273
|
end
|
data/lib/rspec/mocks/methods.rb
CHANGED
@@ -39,7 +39,7 @@ module RSpec
|
|
39
39
|
if chain.length > 1
|
40
40
|
if matching_stub = __mock_proxy.__send__(:find_matching_method_stub, chain[0].to_sym)
|
41
41
|
chain.shift
|
42
|
-
matching_stub.invoke.stub_chain(*chain)
|
42
|
+
matching_stub.invoke.stub_chain(*chain, &blk)
|
43
43
|
else
|
44
44
|
next_in_chain = Object.new
|
45
45
|
stub(chain.shift) { next_in_chain }
|
data/lib/rspec/mocks/mock.rb
CHANGED
@@ -41,10 +41,10 @@ module RSpec
|
|
41
41
|
private
|
42
42
|
|
43
43
|
def method_missing(sym, *args, &block)
|
44
|
+
raise NoMethodError if sym == :to_ary
|
44
45
|
__mock_proxy.record_message_received(sym, *args, &block)
|
45
46
|
begin
|
46
|
-
|
47
|
-
super
|
47
|
+
__mock_proxy.null_object? ? self : super
|
48
48
|
rescue NameError
|
49
49
|
__mock_proxy.raise_unexpected_message_error(sym, *args)
|
50
50
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'rspec/mocks/extensions/marshal'
|
2
|
+
require 'rspec/mocks/extensions/psych' if defined?(::Psych)
|
2
3
|
|
3
4
|
module RSpec
|
4
5
|
module Mocks
|
@@ -8,14 +9,15 @@ module RSpec
|
|
8
9
|
end
|
9
10
|
|
10
11
|
module YAML
|
11
|
-
def to_yaml(
|
12
|
-
return
|
12
|
+
def to_yaml(options = {})
|
13
|
+
return nil if defined?(::Psych) && options.respond_to?(:[]) && options[:nodump]
|
14
|
+
return super(options) unless instance_variable_defined?(:@mock_proxy)
|
13
15
|
|
14
16
|
mp = @mock_proxy
|
15
17
|
remove_instance_variable(:@mock_proxy)
|
16
18
|
|
17
19
|
begin
|
18
|
-
super(
|
20
|
+
super(options)
|
19
21
|
ensure
|
20
22
|
@mock_proxy = mp
|
21
23
|
end
|