rspec-fire 1.2.0 → 1.3.0
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.
- checksums.yaml +7 -0
- data/HISTORY +4 -0
- data/README.md +11 -13
- data/lib/rspec/fire.rb +7 -358
- data/lib/rspec/fire/configuration.rb +20 -0
- data/lib/rspec/fire/legacy.rb +343 -0
- data/rspec-fire.gemspec +2 -2
- data/spec/fire_double_spec.rb +0 -19
- metadata +20 -17
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e86bfffbe18d293983737f7d60713a010565fd33
|
4
|
+
data.tar.gz: 544afff69dfe86663b90a805108e0ddcbfa74d75
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 509aaa64ac7fababd932c9aeae28b935702866eb58efc8232d248c7c399c2594931f71c8217efa38cdbf0e2639c81320d7d335c2a05aea593e7486bf32ad3646
|
7
|
+
data.tar.gz: f0d73156a6f2dc1429a84a3ef0148847cfc58c61e4bba52b8b96fa8f496df084b9549f414db69a5f079b8ad2ddfe325bd0008a1d022910ad1237305fa1c85fcb
|
data/HISTORY
CHANGED
@@ -46,3 +46,7 @@
|
|
46
46
|
* Support `expect` syntax.
|
47
47
|
* Better naming in the API: `instance_double` and `class_double`. Deprecate
|
48
48
|
`fire_double` and `fire_class_double`.
|
49
|
+
|
50
|
+
1.3.0 - 7 November 2013
|
51
|
+
* Support for RSpec 3. When it is loaded, this library is now a no-op since
|
52
|
+
all the functionality (and more!) was ported.
|
data/README.md
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
rspec-fire
|
2
2
|
==========
|
3
3
|
|
4
|
-
|
4
|
+
Checks that stubbed methods on your test double exist, but still allow you to run in isolation
|
5
|
+
when you choose. A failure will be triggered if an invalid method is being stubbed.
|
5
6
|
|
6
7
|
Once,
|
7
8
|
a younger brother came to him,
|
@@ -72,7 +73,7 @@ Specify the class being doubled in your specs:
|
|
72
73
|
describe User, '#suspend!' do
|
73
74
|
it 'sends a notification' do
|
74
75
|
# Only this one line differs from how you write specs normally
|
75
|
-
notifier =
|
76
|
+
notifier = instance_double("EmailNotifier")
|
76
77
|
|
77
78
|
notifier.should_receive(:notify).with("suspended as")
|
78
79
|
|
@@ -117,7 +118,7 @@ A workaround is to explicitly define the methods you are mocking:
|
|
117
118
|
### Doubling constants
|
118
119
|
|
119
120
|
A particularly excellent feature. You can stub out constants using
|
120
|
-
`
|
121
|
+
`class_double`, removing the need to dependency inject
|
121
122
|
collaborators (a technique that can sometimes be cumbersome).
|
122
123
|
|
123
124
|
class User
|
@@ -129,10 +130,7 @@ collaborators (a technique that can sometimes be cumbersome).
|
|
129
130
|
describe User, '#suspend!' do
|
130
131
|
it 'sends a notification' do
|
131
132
|
# Only this one line differs from how you write specs normally
|
132
|
-
notifier =
|
133
|
-
|
134
|
-
# Alternately, you can use this fluent interface
|
135
|
-
notifier = fire_class_double("EmailNotifier").as_replaced_constant
|
133
|
+
notifier = class_double("EmailNotifier").as_stubbed_const
|
136
134
|
|
137
135
|
notifier.should_receive(:notify).with("suspended as")
|
138
136
|
|
@@ -146,7 +144,7 @@ name for it.
|
|
146
144
|
|
147
145
|
### Transferring nested constants to doubled constants
|
148
146
|
|
149
|
-
When you use `
|
147
|
+
When you use `class_double` to replace a class or module
|
150
148
|
that also acts as a namespace for other classes and constants, your
|
151
149
|
access to these constants is cut off for the duration of the example
|
152
150
|
(since the doubled constant does not automatically have all of the
|
@@ -159,23 +157,23 @@ to deal with this:
|
|
159
157
|
end
|
160
158
|
|
161
159
|
# once you do this, you can no longer access MyCoolGem::Widget in your example...
|
162
|
-
|
160
|
+
class_double("MyCoolGem")
|
163
161
|
|
164
162
|
# ...unless you tell rspec-fire to transfer all nested constants
|
165
|
-
|
163
|
+
class_double("MyCoolGem").as_stubbed_const(:transfer_nested_constants => true)
|
166
164
|
|
167
165
|
# ...or give it a list of constants to transfer
|
168
|
-
|
166
|
+
class_double("MyCoolGem").as_stubbed_const(:transfer_nested_constants => [:Widget])
|
169
167
|
|
170
168
|
### Doubling class methods
|
171
169
|
|
172
|
-
Particularly handy for `ActiveRecord` finders. Use `
|
170
|
+
Particularly handy for `ActiveRecord` finders. Use `class_double`. If you
|
173
171
|
dig into the code, you'll find you can create subclasses of `FireDouble` to
|
174
172
|
check for *any* set of methods.
|
175
173
|
|
176
174
|
### Preventing Typo'd Constant Names
|
177
175
|
|
178
|
-
`
|
176
|
+
`instance_double("MyClas")` will not verify any mocked methods, even when
|
179
177
|
`MyClass` is loaded, because of the typo in the constant name. There's
|
180
178
|
an option to help prevent these sorts of fat-finger errors:
|
181
179
|
|
data/lib/rspec/fire.rb
CHANGED
@@ -1,362 +1,11 @@
|
|
1
1
|
require 'rspec/mocks'
|
2
2
|
require 'rspec/expectations'
|
3
|
-
require 'delegate'
|
4
3
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
def initialize
|
12
|
-
self.verify_constant_names = false
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def self.configuration
|
17
|
-
@configuration ||= Configuration.new
|
18
|
-
end
|
19
|
-
|
20
|
-
def self.configure
|
21
|
-
yield configuration
|
22
|
-
end
|
23
|
-
|
24
|
-
Error = Class.new(StandardError)
|
25
|
-
UndefinedConstantError = Class.new(Error)
|
26
|
-
|
27
|
-
class SupportArityMatcher
|
28
|
-
def initialize(arity)
|
29
|
-
@arity = arity
|
30
|
-
end
|
31
|
-
|
32
|
-
attr_reader :arity, :method
|
33
|
-
|
34
|
-
def matches?(method)
|
35
|
-
@method = method
|
36
|
-
min_arity <= arity && arity <= max_arity
|
37
|
-
end
|
38
|
-
|
39
|
-
def failure_message_for_should
|
40
|
-
"Wrong number of arguments for #{method.name}. " +
|
41
|
-
"Expected #{arity_description}, got #{arity}."
|
42
|
-
end
|
43
|
-
|
44
|
-
private
|
45
|
-
|
46
|
-
INFINITY = 1/0.0
|
47
|
-
|
48
|
-
if method(:method).respond_to?(:parameters)
|
49
|
-
def max_arity
|
50
|
-
params = method.parameters
|
51
|
-
return INFINITY if params.any? { |(type, name)| type == :rest } # splat
|
52
|
-
params.count { |(type, name)| type != :block }
|
53
|
-
end
|
54
|
-
else
|
55
|
-
# On 1.8, Method#parameters does not exist.
|
56
|
-
# There's no way to distinguish between default and splat args, so
|
57
|
-
# there's no way to have it work correctly for both default and splat args,
|
58
|
-
# as far as I can tell.
|
59
|
-
# The best we can do is consider it INFINITY (to be tolerant of splat args).
|
60
|
-
def max_arity
|
61
|
-
method.arity < 0 ? INFINITY : method.arity
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
def min_arity
|
66
|
-
return method.arity if method.arity >= 0
|
67
|
-
# ~ inverts the one's complement and gives us the number of required args
|
68
|
-
~method.arity
|
69
|
-
end
|
70
|
-
|
71
|
-
def arity_description
|
72
|
-
return min_arity if min_arity == max_arity
|
73
|
-
return "#{min_arity} or more" if max_arity == INFINITY
|
74
|
-
"#{min_arity} to #{max_arity}"
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
module RecursiveConstMethods
|
79
|
-
# We only want to consider constants that are defined directly on a
|
80
|
-
# particular module, and not include top-level/inherited constants.
|
81
|
-
# Unfortunately, the constant API changed between 1.8 and 1.9, so
|
82
|
-
# we need to conditionally define methods to ignore the top-level/inherited
|
83
|
-
# constants.
|
84
|
-
#
|
85
|
-
# Given `class A; end`:
|
86
|
-
#
|
87
|
-
# On 1.8:
|
88
|
-
# - A.const_get("Hash") # => ::Hash
|
89
|
-
# - A.const_defined?("Hash") # => false
|
90
|
-
# - Neither method accepts the extra `inherit` argument
|
91
|
-
# On 1.9:
|
92
|
-
# - A.const_get("Hash") # => ::Hash
|
93
|
-
# - A.const_defined?("Hash") # => true
|
94
|
-
# - A.const_get("Hash", false) # => raises NameError
|
95
|
-
# - A.const_defined?("Hash", false) # => false
|
96
|
-
if Module.method(:const_defined?).arity == 1
|
97
|
-
def const_defined_on?(mod, const_name)
|
98
|
-
mod.const_defined?(const_name)
|
99
|
-
end
|
100
|
-
|
101
|
-
def get_const_defined_on(mod, const_name)
|
102
|
-
if const_defined_on?(mod, const_name)
|
103
|
-
return mod.const_get(const_name)
|
104
|
-
end
|
105
|
-
|
106
|
-
raise NameError, "uninitialized constant #{mod.name}::#{const_name}"
|
107
|
-
end
|
108
|
-
else
|
109
|
-
def const_defined_on?(mod, const_name)
|
110
|
-
mod.const_defined?(const_name, false)
|
111
|
-
end
|
112
|
-
|
113
|
-
def get_const_defined_on(mod, const_name)
|
114
|
-
mod.const_get(const_name, false)
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
def recursive_const_get name
|
119
|
-
name.split('::').inject(Object) {|klass,name| get_const_defined_on(klass, name) }
|
120
|
-
end
|
121
|
-
|
122
|
-
def recursive_const_defined? name
|
123
|
-
!!name.split('::').inject(Object) {|klass,name|
|
124
|
-
if klass && const_defined_on?(klass, name)
|
125
|
-
get_const_defined_on(klass, name)
|
126
|
-
end
|
127
|
-
}
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
class ShouldProxy < SimpleDelegator
|
132
|
-
include RecursiveConstMethods
|
133
|
-
|
134
|
-
AM = RSpec::Mocks::ArgumentMatchers
|
135
|
-
|
136
|
-
def initialize(double, method_finder, backing)
|
137
|
-
@double = double
|
138
|
-
@method_finder = method_finder
|
139
|
-
@backing = backing
|
140
|
-
@sym = backing.respond_to?(:sym) ? @backing.sym : @backing.message
|
141
|
-
super(backing)
|
142
|
-
end
|
143
|
-
|
144
|
-
def with(*args, &block)
|
145
|
-
unless AM::AnyArgsMatcher === args.first
|
146
|
-
expected_arity = if AM::NoArgsMatcher === args.first
|
147
|
-
0
|
148
|
-
elsif args.length > 0
|
149
|
-
args.length
|
150
|
-
elsif block
|
151
|
-
block.arity
|
152
|
-
else
|
153
|
-
raise ArgumentError.new("No arguments nor block given.")
|
154
|
-
end
|
155
|
-
ensure_arity(expected_arity)
|
156
|
-
end
|
157
|
-
__getobj__.with(*args, &block)
|
158
|
-
end
|
159
|
-
|
160
|
-
protected
|
161
|
-
|
162
|
-
def expect(value)
|
163
|
-
::RSpec::Expectations::ExpectationTarget.new(value)
|
164
|
-
end
|
165
|
-
|
166
|
-
def ensure_arity(actual)
|
167
|
-
@double.with_doubled_class do |klass|
|
168
|
-
expect(klass.__send__(@method_finder, @sym)).to support_arity(actual)
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
def support_arity(arity)
|
173
|
-
SupportArityMatcher.new(arity)
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
module FireDoublable
|
178
|
-
include RecursiveConstMethods
|
179
|
-
|
180
|
-
def should_receive(method_name)
|
181
|
-
ensure_implemented(method_name)
|
182
|
-
ShouldProxy.new(self, @__method_finder, super)
|
183
|
-
end
|
184
|
-
|
185
|
-
def should_not_receive(method_name)
|
186
|
-
ensure_implemented(method_name)
|
187
|
-
super
|
188
|
-
end
|
189
|
-
|
190
|
-
def stub(method_name)
|
191
|
-
ensure_implemented(method_name) unless method_name.is_a?(Hash)
|
192
|
-
super
|
193
|
-
end
|
194
|
-
|
195
|
-
def stub!(method_name)
|
196
|
-
stub(method_name)
|
197
|
-
end
|
198
|
-
|
199
|
-
def with_doubled_class
|
200
|
-
::RSpec::Fire.find_original_value_for(@__doubled_class_name) do |value|
|
201
|
-
yield value if value
|
202
|
-
return
|
203
|
-
end
|
204
|
-
|
205
|
-
if recursive_const_defined?(@__doubled_class_name)
|
206
|
-
yield recursive_const_get(@__doubled_class_name)
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
|
-
protected
|
211
|
-
|
212
|
-
# This cache gives a decent speed up when a class is doubled a lot.
|
213
|
-
def implemented_methods(doubled_class, checked_methods)
|
214
|
-
@@_implemented_methods_cache ||= {}
|
215
|
-
|
216
|
-
# to_sym for non-1.9 compat
|
217
|
-
@@_implemented_methods_cache[[doubled_class, checked_methods]] ||=
|
218
|
-
doubled_class.__send__(checked_methods).map(&:to_sym)
|
219
|
-
end
|
220
|
-
|
221
|
-
def unimplemented_methods(doubled_class, expected_methods, checked_methods)
|
222
|
-
expected_methods.map(&:to_sym) -
|
223
|
-
implemented_methods(doubled_class, checked_methods)
|
224
|
-
end
|
225
|
-
|
226
|
-
def ensure_implemented(*method_names)
|
227
|
-
with_doubled_class do |doubled_class|
|
228
|
-
methods = unimplemented_methods(
|
229
|
-
doubled_class,
|
230
|
-
method_names,
|
231
|
-
@__checked_methods
|
232
|
-
)
|
233
|
-
|
234
|
-
if methods.any?
|
235
|
-
implemented_methods =
|
236
|
-
Object.public_methods -
|
237
|
-
implemented_methods(doubled_class, @__checked_methods)
|
238
|
-
|
239
|
-
msg = "%s does not implement:\n%s" % [
|
240
|
-
doubled_class,
|
241
|
-
methods.sort.map {|x|
|
242
|
-
" #{x}"
|
243
|
-
}.join("\n")
|
244
|
-
|
245
|
-
]
|
246
|
-
raise RSpec::Expectations::ExpectationNotMetError, msg
|
247
|
-
end
|
248
|
-
end
|
249
|
-
end
|
250
|
-
|
251
|
-
def verify_constant_name
|
252
|
-
return if recursive_const_defined?(@__doubled_class_name)
|
253
|
-
|
254
|
-
raise UndefinedConstantError, "#{@__doubled_class_name} is not a defined constant."
|
255
|
-
end
|
256
|
-
end
|
257
|
-
|
258
|
-
class FireObjectDouble < RSpec::Mocks::Mock
|
259
|
-
include FireDoublable
|
260
|
-
|
261
|
-
def initialize(doubled_class, *args)
|
262
|
-
args << {} unless Hash === args.last
|
263
|
-
|
264
|
-
@__doubled_class_name = doubled_class
|
265
|
-
verify_constant_name if RSpec::Fire.configuration.verify_constant_names?
|
266
|
-
|
267
|
-
# __declared_as copied from rspec/mocks definition of `double`
|
268
|
-
args.last[:__declared_as] = 'FireDouble'
|
269
|
-
|
270
|
-
@__checked_methods = :public_instance_methods
|
271
|
-
@__method_finder = :instance_method
|
272
|
-
|
273
|
-
super
|
274
|
-
end
|
275
|
-
end
|
276
|
-
|
277
|
-
class FireClassDouble < Module
|
278
|
-
include FireDoublable
|
279
|
-
|
280
|
-
def initialize(doubled_class, stubs = {})
|
281
|
-
@__doubled_class_name = doubled_class
|
282
|
-
@__checked_methods = :public_methods
|
283
|
-
@__method_finder = :method
|
284
|
-
|
285
|
-
verify_constant_name if RSpec::Fire.configuration.verify_constant_names?
|
286
|
-
|
287
|
-
::RSpec::Mocks::TestDouble.extend_onto self,
|
288
|
-
doubled_class, stubs.merge(:__declared_as => "FireClassDouble")
|
289
|
-
|
290
|
-
# This needs to come after `::RSpec::Mocks::TestDouble.extend_onto`
|
291
|
-
# so that it gets precedence...
|
292
|
-
extend StringRepresentations
|
293
|
-
end
|
294
|
-
|
295
|
-
def as_stubbed_const(options = {})
|
296
|
-
RSpec::Mocks::ConstantStubber.stub(@__doubled_class_name, self, options)
|
297
|
-
@__original_class = RSpec::Mocks::Constant.original(@__doubled_class_name).original_value
|
298
|
-
|
299
|
-
extend AsReplacedConstant
|
300
|
-
self
|
301
|
-
end
|
302
|
-
|
303
|
-
def as_replaced_constant(*args)
|
304
|
-
RSpec::Fire::DEPRECATED["as_replaced_constant is deprecated, use as_stubbed_const instead."]
|
305
|
-
as_stubbed_const(*args)
|
306
|
-
end
|
307
|
-
|
308
|
-
def name
|
309
|
-
@__doubled_class_name
|
310
|
-
end
|
311
|
-
|
312
|
-
module StringRepresentations
|
313
|
-
def to_s
|
314
|
-
@__doubled_class_name + " (fire double)"
|
315
|
-
end
|
316
|
-
|
317
|
-
def inspect
|
318
|
-
to_s
|
319
|
-
end
|
320
|
-
end
|
321
|
-
|
322
|
-
module AsReplacedConstant
|
323
|
-
def with_doubled_class
|
324
|
-
yield @__original_class if @__original_class
|
325
|
-
end
|
326
|
-
end
|
327
|
-
end
|
328
|
-
|
329
|
-
def self.find_original_value_for(constant_name)
|
330
|
-
const = ::RSpec::Mocks::Constant.original(constant_name)
|
331
|
-
yield const.original_value if const.stubbed?
|
332
|
-
end
|
333
|
-
|
334
|
-
def instance_double(*args)
|
335
|
-
FireObjectDouble.new(*args)
|
336
|
-
end
|
337
|
-
|
338
|
-
def class_double(*args)
|
339
|
-
FireClassDouble.new(*args)
|
340
|
-
end
|
341
|
-
|
342
|
-
def fire_double(*args)
|
343
|
-
DEPRECATED["fire_double is deprecated, use instance_double instead."]
|
344
|
-
instance_double(*args)
|
345
|
-
end
|
346
|
-
|
347
|
-
def fire_class_double(*args)
|
348
|
-
DEPRECATED["fire_class_double is deprecated, use class_double instead."]
|
349
|
-
class_double(*args)
|
350
|
-
end
|
351
|
-
|
352
|
-
def fire_replaced_class_double(*args)
|
353
|
-
DEPRECATED["fire_replaced_class_double is deprecated, use class_double with as_stubbed_const instead."]
|
354
|
-
class_double(*args).as_stubbed_const
|
355
|
-
end
|
356
|
-
|
357
|
-
DEPRECATED = lambda do |msg|
|
358
|
-
Kernel.warn caller[2] + ": " + msg
|
359
|
-
end
|
360
|
-
|
361
|
-
end
|
4
|
+
if RSpec::Mocks::Version::STRING.to_f >= 3
|
5
|
+
warn "rspec-fire functionality is now provided by rspec-mocks and is " +
|
6
|
+
"no longer required. You can remove it from your dependencies."
|
7
|
+
else
|
8
|
+
require 'rspec/fire/legacy'
|
362
9
|
end
|
10
|
+
|
11
|
+
require 'rspec/fire/configuration'
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module RSpec
|
2
|
+
module Fire
|
3
|
+
class Configuration
|
4
|
+
attr_accessor :verify_constant_names
|
5
|
+
alias verify_constant_names? verify_constant_names
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
self.verify_constant_names = false
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.configuration
|
13
|
+
@configuration ||= Configuration.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.configure
|
17
|
+
yield configuration
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,343 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module RSpec
|
4
|
+
module Fire
|
5
|
+
Error = Class.new(StandardError)
|
6
|
+
UndefinedConstantError = Class.new(Error)
|
7
|
+
|
8
|
+
class SupportArityMatcher
|
9
|
+
def initialize(arity)
|
10
|
+
@arity = arity
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :arity, :method
|
14
|
+
|
15
|
+
def matches?(method)
|
16
|
+
@method = method
|
17
|
+
min_arity <= arity && arity <= max_arity
|
18
|
+
end
|
19
|
+
|
20
|
+
def failure_message_for_should
|
21
|
+
"Wrong number of arguments for #{method.name}. " +
|
22
|
+
"Expected #{arity_description}, got #{arity}."
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
INFINITY = 1/0.0
|
28
|
+
|
29
|
+
if method(:method).respond_to?(:parameters)
|
30
|
+
def max_arity
|
31
|
+
params = method.parameters
|
32
|
+
return INFINITY if params.any? { |(type, name)| type == :rest } # splat
|
33
|
+
params.count { |(type, name)| type != :block }
|
34
|
+
end
|
35
|
+
else
|
36
|
+
# On 1.8, Method#parameters does not exist.
|
37
|
+
# There's no way to distinguish between default and splat args, so
|
38
|
+
# there's no way to have it work correctly for both default and splat args,
|
39
|
+
# as far as I can tell.
|
40
|
+
# The best we can do is consider it INFINITY (to be tolerant of splat args).
|
41
|
+
def max_arity
|
42
|
+
method.arity < 0 ? INFINITY : method.arity
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def min_arity
|
47
|
+
return method.arity if method.arity >= 0
|
48
|
+
# ~ inverts the one's complement and gives us the number of required args
|
49
|
+
~method.arity
|
50
|
+
end
|
51
|
+
|
52
|
+
def arity_description
|
53
|
+
return min_arity if min_arity == max_arity
|
54
|
+
return "#{min_arity} or more" if max_arity == INFINITY
|
55
|
+
"#{min_arity} to #{max_arity}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
module RecursiveConstMethods
|
60
|
+
# We only want to consider constants that are defined directly on a
|
61
|
+
# particular module, and not include top-level/inherited constants.
|
62
|
+
# Unfortunately, the constant API changed between 1.8 and 1.9, so
|
63
|
+
# we need to conditionally define methods to ignore the top-level/inherited
|
64
|
+
# constants.
|
65
|
+
#
|
66
|
+
# Given `class A; end`:
|
67
|
+
#
|
68
|
+
# On 1.8:
|
69
|
+
# - A.const_get("Hash") # => ::Hash
|
70
|
+
# - A.const_defined?("Hash") # => false
|
71
|
+
# - Neither method accepts the extra `inherit` argument
|
72
|
+
# On 1.9:
|
73
|
+
# - A.const_get("Hash") # => ::Hash
|
74
|
+
# - A.const_defined?("Hash") # => true
|
75
|
+
# - A.const_get("Hash", false) # => raises NameError
|
76
|
+
# - A.const_defined?("Hash", false) # => false
|
77
|
+
if Module.method(:const_defined?).arity == 1
|
78
|
+
def const_defined_on?(mod, const_name)
|
79
|
+
mod.const_defined?(const_name)
|
80
|
+
end
|
81
|
+
|
82
|
+
def get_const_defined_on(mod, const_name)
|
83
|
+
if const_defined_on?(mod, const_name)
|
84
|
+
return mod.const_get(const_name)
|
85
|
+
end
|
86
|
+
|
87
|
+
raise NameError, "uninitialized constant #{mod.name}::#{const_name}"
|
88
|
+
end
|
89
|
+
else
|
90
|
+
def const_defined_on?(mod, const_name)
|
91
|
+
mod.const_defined?(const_name, false)
|
92
|
+
end
|
93
|
+
|
94
|
+
def get_const_defined_on(mod, const_name)
|
95
|
+
mod.const_get(const_name, false)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def recursive_const_get name
|
100
|
+
name.split('::').inject(Object) {|klass,name| get_const_defined_on(klass, name) }
|
101
|
+
end
|
102
|
+
|
103
|
+
def recursive_const_defined? name
|
104
|
+
!!name.split('::').inject(Object) {|klass,name|
|
105
|
+
if klass && const_defined_on?(klass, name)
|
106
|
+
get_const_defined_on(klass, name)
|
107
|
+
end
|
108
|
+
}
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class ShouldProxy < SimpleDelegator
|
113
|
+
include RecursiveConstMethods
|
114
|
+
|
115
|
+
AM = RSpec::Mocks::ArgumentMatchers
|
116
|
+
|
117
|
+
def initialize(double, method_finder, backing)
|
118
|
+
@double = double
|
119
|
+
@method_finder = method_finder
|
120
|
+
@backing = backing
|
121
|
+
@sym = backing.respond_to?(:sym) ? @backing.sym : @backing.message
|
122
|
+
super(backing)
|
123
|
+
end
|
124
|
+
|
125
|
+
def with(*args, &block)
|
126
|
+
unless AM::AnyArgsMatcher === args.first
|
127
|
+
expected_arity = if AM::NoArgsMatcher === args.first
|
128
|
+
0
|
129
|
+
elsif args.length > 0
|
130
|
+
args.length
|
131
|
+
elsif block
|
132
|
+
block.arity
|
133
|
+
else
|
134
|
+
raise ArgumentError.new("No arguments nor block given.")
|
135
|
+
end
|
136
|
+
ensure_arity(expected_arity)
|
137
|
+
end
|
138
|
+
__getobj__.with(*args, &block)
|
139
|
+
end
|
140
|
+
|
141
|
+
protected
|
142
|
+
|
143
|
+
def expect(value)
|
144
|
+
::RSpec::Expectations::ExpectationTarget.new(value)
|
145
|
+
end
|
146
|
+
|
147
|
+
def ensure_arity(actual)
|
148
|
+
@double.with_doubled_class do |klass|
|
149
|
+
expect(klass.__send__(@method_finder, @sym)).to support_arity(actual)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def support_arity(arity)
|
154
|
+
SupportArityMatcher.new(arity)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
module FireDoublable
|
159
|
+
include RecursiveConstMethods
|
160
|
+
|
161
|
+
def should_receive(method_name)
|
162
|
+
ensure_implemented(method_name)
|
163
|
+
ShouldProxy.new(self, @__method_finder, super)
|
164
|
+
end
|
165
|
+
|
166
|
+
def should_not_receive(method_name)
|
167
|
+
ensure_implemented(method_name)
|
168
|
+
super
|
169
|
+
end
|
170
|
+
|
171
|
+
def stub(method_name)
|
172
|
+
ensure_implemented(method_name) unless method_name.is_a?(Hash)
|
173
|
+
super
|
174
|
+
end
|
175
|
+
|
176
|
+
def stub!(method_name)
|
177
|
+
stub(method_name)
|
178
|
+
end
|
179
|
+
|
180
|
+
def with_doubled_class
|
181
|
+
::RSpec::Fire.find_original_value_for(@__doubled_class_name) do |value|
|
182
|
+
yield value if value
|
183
|
+
return
|
184
|
+
end
|
185
|
+
|
186
|
+
if recursive_const_defined?(@__doubled_class_name)
|
187
|
+
yield recursive_const_get(@__doubled_class_name)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
protected
|
192
|
+
|
193
|
+
# This cache gives a decent speed up when a class is doubled a lot.
|
194
|
+
def implemented_methods(doubled_class, checked_methods)
|
195
|
+
@@_implemented_methods_cache ||= {}
|
196
|
+
|
197
|
+
# to_sym for non-1.9 compat
|
198
|
+
@@_implemented_methods_cache[[doubled_class, checked_methods]] ||=
|
199
|
+
doubled_class.__send__(checked_methods).map(&:to_sym)
|
200
|
+
end
|
201
|
+
|
202
|
+
def unimplemented_methods(doubled_class, expected_methods, checked_methods)
|
203
|
+
expected_methods.map(&:to_sym) -
|
204
|
+
implemented_methods(doubled_class, checked_methods)
|
205
|
+
end
|
206
|
+
|
207
|
+
def ensure_implemented(*method_names)
|
208
|
+
with_doubled_class do |doubled_class|
|
209
|
+
methods = unimplemented_methods(
|
210
|
+
doubled_class,
|
211
|
+
method_names,
|
212
|
+
@__checked_methods
|
213
|
+
)
|
214
|
+
|
215
|
+
if methods.any?
|
216
|
+
implemented_methods =
|
217
|
+
Object.public_methods -
|
218
|
+
implemented_methods(doubled_class, @__checked_methods)
|
219
|
+
|
220
|
+
msg = "%s does not implement:\n%s" % [
|
221
|
+
doubled_class,
|
222
|
+
methods.sort.map {|x|
|
223
|
+
" #{x}"
|
224
|
+
}.join("\n")
|
225
|
+
|
226
|
+
]
|
227
|
+
raise RSpec::Expectations::ExpectationNotMetError, msg
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def verify_constant_name
|
233
|
+
return if recursive_const_defined?(@__doubled_class_name)
|
234
|
+
|
235
|
+
raise UndefinedConstantError, "#{@__doubled_class_name} is not a defined constant."
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
class FireObjectDouble < RSpec::Mocks::Mock
|
240
|
+
include FireDoublable
|
241
|
+
|
242
|
+
def initialize(doubled_class, *args)
|
243
|
+
args << {} unless Hash === args.last
|
244
|
+
|
245
|
+
@__doubled_class_name = doubled_class
|
246
|
+
verify_constant_name if RSpec::Fire.configuration.verify_constant_names?
|
247
|
+
|
248
|
+
# __declared_as copied from rspec/mocks definition of `double`
|
249
|
+
args.last[:__declared_as] = 'FireDouble'
|
250
|
+
|
251
|
+
@__checked_methods = :public_instance_methods
|
252
|
+
@__method_finder = :instance_method
|
253
|
+
|
254
|
+
super
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
class FireClassDouble < Module
|
259
|
+
include FireDoublable
|
260
|
+
|
261
|
+
def initialize(doubled_class, stubs = {})
|
262
|
+
@__doubled_class_name = doubled_class
|
263
|
+
@__checked_methods = :public_methods
|
264
|
+
@__method_finder = :method
|
265
|
+
|
266
|
+
verify_constant_name if RSpec::Fire.configuration.verify_constant_names?
|
267
|
+
|
268
|
+
::RSpec::Mocks::TestDouble.extend_onto self,
|
269
|
+
doubled_class, stubs.merge(:__declared_as => "FireClassDouble")
|
270
|
+
|
271
|
+
# This needs to come after `::RSpec::Mocks::TestDouble.extend_onto`
|
272
|
+
# so that it gets precedence...
|
273
|
+
extend StringRepresentations
|
274
|
+
end
|
275
|
+
|
276
|
+
def as_stubbed_const(options = {})
|
277
|
+
RSpec::Mocks::ConstantStubber.stub(@__doubled_class_name, self, options)
|
278
|
+
@__original_class = RSpec::Mocks::Constant.original(@__doubled_class_name).original_value
|
279
|
+
|
280
|
+
extend AsReplacedConstant
|
281
|
+
self
|
282
|
+
end
|
283
|
+
|
284
|
+
def as_replaced_constant(*args)
|
285
|
+
RSpec::Fire::DEPRECATED["as_replaced_constant is deprecated, use as_stubbed_const instead."]
|
286
|
+
as_stubbed_const(*args)
|
287
|
+
end
|
288
|
+
|
289
|
+
def name
|
290
|
+
@__doubled_class_name
|
291
|
+
end
|
292
|
+
|
293
|
+
module StringRepresentations
|
294
|
+
def to_s
|
295
|
+
@__doubled_class_name + " (fire double)"
|
296
|
+
end
|
297
|
+
|
298
|
+
def inspect
|
299
|
+
to_s
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
module AsReplacedConstant
|
304
|
+
def with_doubled_class
|
305
|
+
yield @__original_class if @__original_class
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
def self.find_original_value_for(constant_name)
|
311
|
+
const = ::RSpec::Mocks::Constant.original(constant_name)
|
312
|
+
yield const.original_value if const.stubbed?
|
313
|
+
end
|
314
|
+
|
315
|
+
def instance_double(*args)
|
316
|
+
FireObjectDouble.new(*args)
|
317
|
+
end
|
318
|
+
|
319
|
+
def class_double(*args)
|
320
|
+
FireClassDouble.new(*args)
|
321
|
+
end
|
322
|
+
|
323
|
+
def fire_double(*args)
|
324
|
+
DEPRECATED["fire_double is deprecated, use instance_double instead."]
|
325
|
+
instance_double(*args)
|
326
|
+
end
|
327
|
+
|
328
|
+
def fire_class_double(*args)
|
329
|
+
DEPRECATED["fire_class_double is deprecated, use class_double instead."]
|
330
|
+
class_double(*args)
|
331
|
+
end
|
332
|
+
|
333
|
+
def fire_replaced_class_double(*args)
|
334
|
+
DEPRECATED["fire_replaced_class_double is deprecated, use class_double with as_stubbed_const instead."]
|
335
|
+
class_double(*args).as_stubbed_const
|
336
|
+
end
|
337
|
+
|
338
|
+
DEPRECATED = lambda do |msg|
|
339
|
+
Kernel.warn caller[2] + ": " + msg
|
340
|
+
end
|
341
|
+
|
342
|
+
end
|
343
|
+
end
|
data/rspec-fire.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'rspec-fire'
|
3
|
-
s.version = '1.
|
3
|
+
s.version = '1.3.0'
|
4
4
|
s.summary = 'More resilient test doubles for RSpec.'
|
5
5
|
s.platform = Gem::Platform::RUBY
|
6
6
|
s.authors = ["Xavier Shay"]
|
@@ -19,6 +19,6 @@ Gem::Specification.new do |s|
|
|
19
19
|
rspec-fire.gemspec
|
20
20
|
)
|
21
21
|
|
22
|
-
s.add_dependency 'rspec', '
|
22
|
+
s.add_dependency 'rspec', ['>= 2.11', '< 4']
|
23
23
|
s.add_development_dependency 'rake'
|
24
24
|
end
|
data/spec/fire_double_spec.rb
CHANGED
@@ -166,25 +166,6 @@ shared_examples_for 'a fire-enhanced double' do
|
|
166
166
|
let(:method_under_test) { :should_not_receive }
|
167
167
|
it_should_behave_like 'a fire-enhanced double method'
|
168
168
|
end
|
169
|
-
|
170
|
-
[ :stub, :stub! ].each do |stubber|
|
171
|
-
describe "##{stubber}" do
|
172
|
-
let(:method_under_test) { stubber }
|
173
|
-
it_should_behave_like 'a fire-enhanced double method'
|
174
|
-
|
175
|
-
context "RSpec's hash shortcut syntax" do
|
176
|
-
context 'doubled class is not loaded' do
|
177
|
-
let(:doubled_object) { instance_double("UnloadedObject") }
|
178
|
-
should_allow(:undefined_method => 123)
|
179
|
-
end
|
180
|
-
|
181
|
-
context 'doubled class is loaded' do
|
182
|
-
should_allow(:defined_method => 456)
|
183
|
-
should_not_allow(:undefined_method => 789)
|
184
|
-
end
|
185
|
-
end
|
186
|
-
end
|
187
|
-
end
|
188
169
|
end
|
189
170
|
|
190
171
|
describe '#instance_double' do
|
metadata
CHANGED
@@ -1,46 +1,47 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rspec-fire
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
5
|
-
prerelease:
|
4
|
+
version: 1.3.0
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Xavier Shay
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date: 2013-
|
11
|
+
date: 2013-11-07 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: rspec
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
16
|
requirements:
|
19
|
-
- -
|
17
|
+
- - '>='
|
20
18
|
- !ruby/object:Gem::Version
|
21
19
|
version: '2.11'
|
20
|
+
- - <
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '4'
|
22
23
|
type: :runtime
|
23
24
|
prerelease: false
|
24
25
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
26
|
requirements:
|
27
|
-
- -
|
27
|
+
- - '>='
|
28
28
|
- !ruby/object:Gem::Version
|
29
29
|
version: '2.11'
|
30
|
+
- - <
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '4'
|
30
33
|
- !ruby/object:Gem::Dependency
|
31
34
|
name: rake
|
32
35
|
requirement: !ruby/object:Gem::Requirement
|
33
|
-
none: false
|
34
36
|
requirements:
|
35
|
-
- -
|
37
|
+
- - '>='
|
36
38
|
- !ruby/object:Gem::Version
|
37
39
|
version: '0'
|
38
40
|
type: :development
|
39
41
|
prerelease: false
|
40
42
|
version_requirements: !ruby/object:Gem::Requirement
|
41
|
-
none: false
|
42
43
|
requirements:
|
43
|
-
- -
|
44
|
+
- - '>='
|
44
45
|
- !ruby/object:Gem::Version
|
45
46
|
version: '0'
|
46
47
|
description:
|
@@ -52,6 +53,8 @@ extra_rdoc_files: []
|
|
52
53
|
files:
|
53
54
|
- spec/fire_double_spec.rb
|
54
55
|
- spec/spec_helper.rb
|
56
|
+
- lib/rspec/fire/configuration.rb
|
57
|
+
- lib/rspec/fire/legacy.rb
|
55
58
|
- lib/rspec/fire.rb
|
56
59
|
- Gemfile
|
57
60
|
- README.md
|
@@ -61,26 +64,26 @@ files:
|
|
61
64
|
homepage: http://github.com/xaviershay/rspec-fire
|
62
65
|
licenses:
|
63
66
|
- MIT
|
67
|
+
metadata: {}
|
64
68
|
post_install_message:
|
65
69
|
rdoc_options: []
|
66
70
|
require_paths:
|
67
71
|
- lib
|
68
72
|
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
-
none: false
|
70
73
|
requirements:
|
71
|
-
- -
|
74
|
+
- - '>='
|
72
75
|
- !ruby/object:Gem::Version
|
73
76
|
version: '0'
|
74
77
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
-
none: false
|
76
78
|
requirements:
|
77
|
-
- -
|
79
|
+
- - '>='
|
78
80
|
- !ruby/object:Gem::Version
|
79
81
|
version: '0'
|
80
82
|
requirements: []
|
81
83
|
rubyforge_project:
|
82
|
-
rubygems_version:
|
84
|
+
rubygems_version: 2.0.3
|
83
85
|
signing_key:
|
84
|
-
specification_version:
|
86
|
+
specification_version: 4
|
85
87
|
summary: More resilient test doubles for RSpec.
|
86
88
|
test_files: []
|
89
|
+
has_rdoc: false
|