rspec-fire 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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