rspec-fire 0.3.0 → 0.4.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.
data/HISTORY CHANGED
@@ -12,3 +12,11 @@
12
12
 
13
13
  0.3.0 - 17 March 2012
14
14
  * Added support for doubling constants.
15
+
16
+ 0.4.0 - 1 April 2012
17
+ * Handle default args and splats correctly.
18
+ * rspec-mocks 2.9-master support.
19
+ * Allow `fire_double("Class", :foo => 17)` syntax.
20
+ * Allow `mock.stub(:foo => 17)` syntax.
21
+ * Don't count block params when determining max arity.
22
+ * Restore original const value when a const is stubbed more than once.
data/lib/rspec/fire.rb CHANGED
@@ -4,6 +4,57 @@ require 'delegate'
4
4
 
5
5
  module RSpec
6
6
  module Fire
7
+ class SupportArityMatcher
8
+ def initialize(arity)
9
+ @arity = arity
10
+ end
11
+
12
+ attr_reader :arity, :method
13
+
14
+ def matches?(method)
15
+ @method = method
16
+ min_arity <= arity && arity <= max_arity
17
+ end
18
+
19
+ def failure_message_for_should
20
+ "Wrong number of arguments for #{method.name}. " +
21
+ "Expected #{arity_description}, got #{arity}."
22
+ end
23
+
24
+ private
25
+
26
+ INFINITY = 1/0.0
27
+
28
+ if method(:method).respond_to?(:parameters)
29
+ def max_arity
30
+ params = method.parameters
31
+ return INFINITY if params.any? { |(type, name)| type == :rest } # splat
32
+ params.count { |(type, name)| type != :block }
33
+ end
34
+ else
35
+ # On 1.8, Method#parameters does not exist.
36
+ # There's no way to distinguish between default and splat args, so
37
+ # there's no way to have it work correctly for both default and splat args,
38
+ # as far as I can tell.
39
+ # The best we can do is consider it INFINITY (to be tolerant of splat args).
40
+ def max_arity
41
+ method.arity < 0 ? INFINITY : method.arity
42
+ end
43
+ end
44
+
45
+ def min_arity
46
+ return method.arity if method.arity >= 0
47
+ # ~ inverts the one's complement and gives us the number of required args
48
+ ~method.arity
49
+ end
50
+
51
+ def arity_description
52
+ return min_arity if min_arity == max_arity
53
+ return "#{min_arity} or more" if max_arity == INFINITY
54
+ "#{min_arity} to #{max_arity}"
55
+ end
56
+ end
57
+
7
58
  module RecursiveConstMethods
8
59
  def recursive_const_get name
9
60
  name.split('::').inject(Object) {|klass,name| klass.const_get name }
@@ -19,7 +70,6 @@ module RSpec
19
70
  end
20
71
 
21
72
  class ShouldProxy < SimpleDelegator
22
- extend RSpec::Matchers::DSL
23
73
  include RecursiveConstMethods
24
74
 
25
75
  AM = RSpec::Mocks::ArgumentMatchers
@@ -52,19 +102,12 @@ module RSpec
52
102
 
53
103
  def ensure_arity(actual)
54
104
  @double.with_doubled_class do |klass|
55
- klass.send(@method_finder, @sym).should have_arity(actual)
105
+ klass.send(@method_finder, @sym).should support_arity(actual)
56
106
  end
57
107
  end
58
108
 
59
- define :have_arity do |actual|
60
- match do |method|
61
- method.arity >= 0 && method.arity == actual
62
- end
63
-
64
- failure_message_for_should do |method|
65
- "Wrong number of arguments for #{method.name}. " +
66
- "Expected #{method.arity}, got #{actual}."
67
- end
109
+ def support_arity(arity)
110
+ SupportArityMatcher.new(arity)
68
111
  end
69
112
  end
70
113
 
@@ -83,12 +126,18 @@ module RSpec
83
126
  end
84
127
 
85
128
  def stub(method_name)
86
- ensure_implemented(method_name)
129
+ ensure_implemented(method_name) unless method_name.is_a?(Hash)
87
130
  super
88
131
  end
89
132
 
133
+ def stub!(method_name)
134
+ stub(method_name)
135
+ end
136
+
90
137
  def with_doubled_class
91
- if recursive_const_defined?(@__doubled_class_name)
138
+ if original_stubbed_const_value = ConstantStubber.original_value_for(@__doubled_class_name)
139
+ yield original_stubbed_const_value
140
+ elsif recursive_const_defined?(@__doubled_class_name)
92
141
  yield recursive_const_get(@__doubled_class_name)
93
142
  end
94
143
  end
@@ -135,9 +184,11 @@ module RSpec
135
184
 
136
185
  # __declared_as copied from rspec/mocks definition of `double`
137
186
  args.last[:__declared_as] = 'FireDouble'
138
- super
187
+
139
188
  @__checked_methods = :public_instance_methods
140
189
  @__method_finder = :instance_method
190
+
191
+ super
141
192
  end
142
193
  end
143
194
 
@@ -150,8 +201,20 @@ module RSpec
150
201
  @__checked_methods = :public_methods
151
202
  @__method_finder = :method
152
203
 
153
- stubs.each do |message, response|
154
- stub(message).and_return(response)
204
+ # TestDouble was added after rspec 2.9.0, and allows proper mocking
205
+ # of public methods that have clashing private methods. See spec for
206
+ # details.
207
+ if defined?(::RSpec::Mocks::TestDouble)
208
+ ::RSpec::Mocks::TestDouble.extend_onto self,
209
+ doubled_class, stubs.merge(:__declared_as => "FireClassDouble")
210
+ else
211
+ stubs.each do |message, response|
212
+ stub(message).and_return(response)
213
+ end
214
+
215
+ def self.method_missing(name, *args)
216
+ __mock_proxy.raise_unexpected_message_error(name, *args)
217
+ end
155
218
  end
156
219
 
157
220
  def self.as_replaced_constant(options = {})
@@ -171,10 +234,6 @@ module RSpec
171
234
  def self.name
172
235
  @__doubled_class_name
173
236
  end
174
-
175
- def self.method_missing(name, *args)
176
- __mock_proxy.raise_unexpected_message_error(name, *args)
177
- end
178
237
  end
179
238
  end
180
239
 
@@ -190,7 +249,7 @@ module RSpec
190
249
 
191
250
  class DefinedConstantReplacer
192
251
  include RecursiveConstMethods
193
- attr_reader :original_value
252
+ attr_reader :original_value, :full_constant_name
194
253
 
195
254
  def initialize(full_constant_name, stubbed_value, transfer_nested_constants)
196
255
  @full_constant_name = full_constant_name
@@ -259,6 +318,8 @@ module RSpec
259
318
  class UndefinedConstantSetter
260
319
  include RecursiveConstMethods
261
320
 
321
+ attr_reader :full_constant_name
322
+
262
323
  def initialize(full_constant_name, stubbed_value)
263
324
  @full_constant_name = full_constant_name
264
325
  @stubbed_value = stubbed_value
@@ -301,10 +362,38 @@ module RSpec
301
362
  UndefinedConstantSetter.new(constant_name, value)
302
363
  end
303
364
 
365
+ stubbers << stubber
366
+
304
367
  stubber.stub!
305
- ::RSpec::Mocks.space.add(stubber)
368
+ ensure_registered_with_rspec_mocks
306
369
  stubber.original_value
307
370
  end
371
+
372
+ def self.ensure_registered_with_rspec_mocks
373
+ return if @registered_with_rspec_mocks
374
+ ::RSpec::Mocks.space.add(self)
375
+ @registered_with_rspec_mocks = true
376
+ end
377
+
378
+ def self.rspec_reset
379
+ @registered_with_rspec_mocks = false
380
+
381
+ # We use reverse order so that if the same constant
382
+ # was stubbed multiple times, the original value gets
383
+ # properly restored.
384
+ stubbers.reverse.each { |s| s.rspec_reset }
385
+
386
+ stubbers.clear
387
+ end
388
+
389
+ def self.stubbers
390
+ @stubbers ||= []
391
+ end
392
+
393
+ def self.original_value_for(constant_name)
394
+ stubber = stubbers.find { |s| s.full_constant_name == constant_name }
395
+ stubber.original_value if stubber
396
+ end
308
397
  end
309
398
 
310
399
  def stub_const(name, value, options = {})
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 = '0.3.0'
3
+ s.version = '0.4.0'
4
4
  s.summary = 'More resilient test doubles for RSpec.'
5
5
  s.platform = Gem::Platform::RUBY
6
6
  s.authors = ["Xavier Shay"]
@@ -1,14 +1,9 @@
1
1
  require 'spec_helper'
2
2
 
3
- TOP_LEVEL_VALUE_CONST = 7
3
+ def use; end
4
+ private :use
4
5
 
5
- RSpec::Mocks::Space.class_eval do
6
- # Deal with the fact that #mocks was renamed to #receivers for RSpec 2.9:
7
- # https://github.com/rspec/rspec-mocks/commit/17c259ea5143d309e90ca6d53d40f6356ac2d0a5
8
- unless private_instance_methods.map(&:to_sym).include?(:receivers)
9
- alias_method :receivers, :mocks
10
- end
11
- end
6
+ TOP_LEVEL_VALUE_CONST = 7
12
7
 
13
8
  module TestMethods
14
9
  def defined_method
@@ -25,6 +20,7 @@ class TestObject
25
20
  end
26
21
 
27
22
  class TestClass
23
+ include TestMethods
28
24
  extend TestMethods
29
25
 
30
26
  M = :m
@@ -34,6 +30,10 @@ class TestClass
34
30
  class NestedEvenMore
35
31
  end
36
32
  end
33
+
34
+ def self.use
35
+ raise "Y U NO MOCK?"
36
+ end
37
37
  end
38
38
 
39
39
  shared_examples_for 'a fire-enhanced double method' do
@@ -49,20 +49,26 @@ shared_examples_for 'a fire-enhanced double method' do
49
49
  end
50
50
 
51
51
  shared_examples_for 'a fire-enhanced double' do
52
- def self.should_allow(method_name)
53
- it "should allow #{method_name}" do
52
+ def self.should_allow(method_parameters)
53
+ method_to_stub = method_parameters.is_a?(Hash) ?
54
+ method_parameters.keys.first : method_parameters
55
+
56
+ it "should allow #{method_to_stub}" do
54
57
  lambda {
55
- doubled_object.send(method_under_test, method_name)
58
+ doubled_object.send(method_under_test, method_parameters)
56
59
  }.should_not raise_error
57
60
  doubled_object.rspec_reset
58
61
  end
59
62
  end
60
63
 
61
- def self.should_not_allow(method_name)
62
- it "should not allow #{method_name}" do
64
+ def self.should_not_allow(method_parameters)
65
+ method_to_stub = method_parameters.is_a?(Hash) ?
66
+ method_parameters.keys.first : method_parameters
67
+
68
+ it "should not allow #{method_to_stub}" do
63
69
  lambda {
64
- doubled_object.send(method_under_test, method_name)
65
- }.should fail_matching("does not implement", method_name)
70
+ doubled_object.send(method_under_test, method_parameters)
71
+ }.should fail_matching("does not implement", method_to_stub)
66
72
  end
67
73
  end
68
74
 
@@ -136,9 +142,23 @@ shared_examples_for 'a fire-enhanced double' do
136
142
  it_should_behave_like 'a fire-enhanced double method'
137
143
  end
138
144
 
139
- describe '#stub' do
140
- let(:method_under_test) { :stub }
141
- it_should_behave_like 'a fire-enhanced double method'
145
+ [ :stub, :stub! ].each do |stubber|
146
+ describe "##{stubber}" do
147
+ let(:method_under_test) { stubber }
148
+ it_should_behave_like 'a fire-enhanced double method'
149
+
150
+ context "RSpec's hash shortcut syntax" do
151
+ context 'doubled class is not loaded' do
152
+ let(:doubled_object) { fire_double("UnloadedObject") }
153
+ should_allow(:undefined_method => 123)
154
+ end
155
+
156
+ context 'doubled class is loaded' do
157
+ should_allow(:defined_method => 456)
158
+ should_not_allow(:undefined_method => 789)
159
+ end
160
+ end
161
+ end
142
162
  end
143
163
  end
144
164
 
@@ -146,6 +166,11 @@ describe '#fire_double' do
146
166
  let(:doubled_object) { fire_double("TestObject") }
147
167
 
148
168
  it_should_behave_like 'a fire-enhanced double'
169
+
170
+ it 'allows stubs to be passed as a hash' do
171
+ double = fire_double("TestObject", :defined_method => 17)
172
+ double.defined_method.should eq(17)
173
+ end
149
174
  end
150
175
 
151
176
  describe '#fire_class_double' do
@@ -178,6 +203,16 @@ describe '#fire_class_double' do
178
203
  double.a.should eq(5)
179
204
  double.b.should eq(8)
180
205
  end
206
+
207
+ it 'allows private methods to be stubbed, just like on a normal test double (but unlike a partial mock)' do
208
+ mod = Module.new
209
+ mod.stub(:use)
210
+ expect { mod.use }.to raise_error(/private method `use' called/)
211
+
212
+ fire_double = fire_class_double("TestClass")
213
+ fire_double.stub(:use)
214
+ fire_double.use # should not raise an error
215
+ end if defined?(::RSpec::Mocks::TestDouble)
181
216
  end
182
217
 
183
218
  def reset_rspec_mocks
@@ -202,6 +237,31 @@ describe '#fire_replaced_class_double (for an existing class)' do
202
237
  TestClass::M.should eq(:m)
203
238
  TestClass::N.should eq(:n)
204
239
  end
240
+
241
+ def use_doubles(class_double, instance_double)
242
+ instance_double.should_receive(:defined_method).and_return(3)
243
+ class_double.should_receive(:defined_method).and_return(4)
244
+
245
+ instance_double.defined_method.should eq(3)
246
+ class_double.defined_method.should eq(4)
247
+
248
+ expect { instance_double.should_receive(:undefined_method) }.to fail_matching("does not implement")
249
+ expect { class_double.should_receive(:undefined_method) }.to fail_matching("does not implement")
250
+ end
251
+
252
+ it 'can be used after a declared fire_double for the same class' do
253
+ instance_double = fire_double("TestClass")
254
+ class_double = fire_replaced_class_double("TestClass")
255
+
256
+ use_doubles class_double, instance_double
257
+ end
258
+
259
+ it 'can be used before a declared fire_double for the same class' do
260
+ class_double = fire_replaced_class_double("TestClass")
261
+ instance_double = fire_double("TestClass")
262
+
263
+ use_doubles class_double, instance_double
264
+ end
205
265
  end
206
266
 
207
267
  describe '#fire_replaced_class_double (for a non-existant class)' do
@@ -297,7 +357,7 @@ shared_examples_for "unloaded constant stubbing" do |const_name|
297
357
  it 'does not remove the constant when the example manually sets it' do
298
358
  begin
299
359
  stub_const(const_name, 7)
300
- stubber = RSpec::Mocks.space.send(:receivers).first
360
+ stubber = RSpec::Fire::ConstantStubber.stubbers.first
301
361
  change_const_value_to(new_const_value = Object.new)
302
362
  reset_rspec_mocks
303
363
  const.should equal(new_const_value)
@@ -322,6 +382,16 @@ describe "#stub_const" do
322
382
  context 'for a loaded unnested constant' do
323
383
  it_behaves_like "loaded constant stubbing", "TestClass"
324
384
 
385
+ it 'can be stubbed multiple times but still restores the original value properly' do
386
+ orig_value = TestClass
387
+ stub1, stub2 = Module.new, Module.new
388
+ stub_const("TestClass", stub1)
389
+ stub_const("TestClass", stub2)
390
+
391
+ reset_rspec_mocks
392
+ TestClass.should be(orig_value)
393
+ end
394
+
325
395
  it 'allows nested constants to be transferred to a stub module' do
326
396
  tc_nested = TestClass::Nested
327
397
  stub = Module.new
@@ -443,3 +513,86 @@ describe "#stub_const" do
443
513
  end
444
514
  end
445
515
  end
516
+
517
+ describe RSpec::Fire::SupportArityMatcher do
518
+ def support_arity(arity)
519
+ RSpec::Fire::SupportArityMatcher.new(arity)
520
+ end
521
+
522
+ context "a method with an exact arity" do
523
+ def two_args(a, b); end
524
+ def no_args; end
525
+
526
+ it 'passes when given the correct arity' do
527
+ method(:two_args).should support_arity(2)
528
+ method(:no_args).should support_arity(0)
529
+ end
530
+
531
+ it 'fails when given the wrong arity' do
532
+ expect {
533
+ method(:no_args).should support_arity(1)
534
+ }.to raise_error(/Expected 0, got 1/)
535
+
536
+ expect {
537
+ method(:two_args).should support_arity(1)
538
+ }.to raise_error(/Expected 2, got 1/)
539
+ end
540
+ end
541
+
542
+ context "a method with one required arg and two default args" do
543
+ def m(a, b=5, c=2); end
544
+
545
+ it 'passes when given 1 to 3 args' do
546
+ method(:m).should support_arity(1)
547
+ method(:m).should support_arity(2)
548
+ method(:m).should support_arity(3)
549
+ end
550
+
551
+ let(:can_distinguish_splat_from_defaults?) { method(:method).respond_to?(:parameters) }
552
+
553
+ it 'fails when given 0' do
554
+ pending("1.8 cannot distinguish default args from splats", :unless => can_distinguish_splat_from_defaults?) do
555
+ expect {
556
+ method(:m).should support_arity(0)
557
+ }.to raise_error(/Expected 1 to 3, got 0/)
558
+ end
559
+ end
560
+
561
+ it 'fails when given more than 3' do
562
+ pending("1.8 cannot distinguish default args from splats", :unless => can_distinguish_splat_from_defaults?) do
563
+ expect {
564
+ method(:m).should support_arity(4)
565
+ }.to raise_error(/Expected 1 to 3, got 4/)
566
+ end
567
+ end
568
+ end
569
+
570
+ context "a method with one required arg and a splat" do
571
+ def m(a, *b); end
572
+
573
+ it 'passes when given 1 or more' do
574
+ method(:m).should support_arity(1)
575
+ method(:m).should support_arity(20)
576
+ end
577
+
578
+ it 'fails when given 0' do
579
+ expect {
580
+ method(:m).should support_arity(0)
581
+ }.to raise_error(/Expected 1 or more, got 0/)
582
+ end
583
+ end
584
+
585
+ context "a method with an explicit block arg" do
586
+ def m(a, &b); end
587
+
588
+ it 'passes when given 1' do
589
+ method(:m).should support_arity(1)
590
+ end
591
+
592
+ it 'fails when given 2' do
593
+ expect {
594
+ method(:m).should support_arity(2)
595
+ }.to raise_error(/Expected 1, got 2/)
596
+ end
597
+ end
598
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-fire
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-03-18 00:00:00.000000000 Z
12
+ date: 2012-04-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
16
- requirement: &2153330640 !ruby/object:Gem::Requirement
16
+ requirement: &2160595040 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *2153330640
24
+ version_requirements: *2160595040
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rspec
27
- requirement: &2153330120 !ruby/object:Gem::Requirement
27
+ requirement: &2160594520 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,7 +32,7 @@ dependencies:
32
32
  version: '2.5'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *2153330120
35
+ version_requirements: *2160594520
36
36
  description:
37
37
  email:
38
38
  - hello@xaviershay.com