rspec-fire 0.1 → 0.2

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
@@ -1,2 +1,5 @@
1
1
  0.1 - 3 September 2011
2
2
  * Initial release
3
+
4
+ 0.2 - 3 September 2011
5
+ * Checks method arity when a 'with' call is present
data/README.md CHANGED
@@ -23,13 +23,28 @@ Making your test doubles more resilient.
23
23
 
24
24
  -- Desert Way, Charlie Hunter
25
25
 
26
- Test doubles are sweet for isolating your unit tests, but we lost something in the translation from typed languages. Ruby doesn't have a compiler that can verify the contracts being mocked out are indeed legit. This hurts larger refactorings, since you can totally change a collaborator --- renaming methods, changing the number of arguments --- and all the mocks that were standing in for it will keep pretending everything is ok.
27
-
28
- `rspec-fire` mitigates that problem, with very little change to your existing coding style.
29
-
30
- One solution would be to disallow stubbing of methods that don't exist. This is what mocha does with its `Mocha::Configuration.prevent(:stubbing_non_existent_method)` option. The downside is, you now have to load the collaborators/dependencies that you are mocking, which kind of defeats the purpose of isolated testing. Not ideal.
31
-
32
- Another solution, that `rspec-fire` adopts, is a more relaxed version that only checks that the methods exist _if the doubled class has already been loaded_. No checking will happen when running the spec in isolation, but when run in the context of the full app (either as a full spec run or by explicitly preloading collaborators on the command line) a failure will be triggered if an invalid method is being stubbed.
26
+ Test doubles are sweet for isolating your unit tests, but we lost something in
27
+ the translation from typed languages. Ruby doesn't have a compiler that can
28
+ verify the contracts being mocked out are indeed legit. This hurts larger
29
+ refactorings, since you can totally change a collaborator --- renaming methods,
30
+ changing the number of arguments --- and all the mocks that were standing in
31
+ for it will keep pretending everything is ok.
32
+
33
+ `rspec-fire` mitigates that problem, with very little change to your existing
34
+ coding style.
35
+
36
+ One solution would be to disallow stubbing of methods that don't exist. This is
37
+ what mocha does with its
38
+ `Mocha::Configuration.prevent(:stubbing_non_existent_method)` option. The
39
+ downside is, you now have to load the collaborators/dependencies that you are
40
+ mocking, which kind of defeats the purpose of isolated testing. Not ideal.
41
+
42
+ Another solution, that `rspec-fire` adopts, is a more relaxed version that only
43
+ checks that the methods exist _if the doubled class has already been loaded_.
44
+ No checking will happen when running the spec in isolation, but when run in the
45
+ context of the full app (either as a full spec run or by explicitly preloading
46
+ collaborators on the command line) a failure will be triggered if an invalid
47
+ method is being stubbed.
33
48
 
34
49
  Usage
35
50
  -----
@@ -62,38 +77,52 @@ Specify the class being doubled in your specs:
62
77
 
63
78
  Run your specs:
64
79
 
65
- rspec spec/user_spec.rb # Isolated, will pass always
66
- rspec -Ilib/email_notifier.rb spec/user_spec.rb # Will fail if EmailNotifier#notify method is not defined
80
+ # Isolated, will pass always
81
+ rspec spec/user_spec.rb
67
82
 
68
- Currently only method presence/absense is checked, but theoretically arity can be checked also.
83
+ # Will fail if EmailNotifier#notify method is not defined
84
+ rspec -Ilib/email_notifier.rb spec/user_spec.rb
85
+
86
+ Method presence/absence is checked, and if a `with` is provided then so is
87
+ arity.
69
88
 
70
89
  Protips
71
90
  -------
72
91
 
73
92
  ### Using with an existing Rails project
74
93
 
75
- Create a new file `unit_helper.rb` that _does not_ require `spec_helper.rb`. Require this file where needed for isolated tests. To run an isolated spec in the context of your app:
94
+ Create a new file `unit_helper.rb` that _does not_ require `spec_helper.rb`.
95
+ Require this file where needed for isolated tests. To run an isolated spec in
96
+ the context of your app:
76
97
 
77
98
  rspec -rspec/spec_helper.rb spec/unit/my_spec.rb
78
99
 
79
100
  ### Doubling class methods
80
101
 
81
- Particularly handy for `ActiveRecord` finders. Use `fire_class_double`. If you dig into the code, you'll find you can create subclasses of `FireDouble` to check for *any* set of methods.
102
+ Particularly handy for `ActiveRecord` finders. Use `fire_class_double`. If you
103
+ dig into the code, you'll find you can create subclasses of `FireDouble` to
104
+ check for *any* set of methods.
82
105
 
83
106
  ### Mocking Done Right (tm)
84
107
 
85
108
  * Only mock methods on collaborators, _not_ the class under test.
86
109
  * Only mock public methods.
87
- * Extract common mock setup to keep your specs [DRY](http://en.wikipedia.org/wiki/DRY).
110
+ * Extract common mock setup to keep your specs
111
+ [DRY](http://en.wikipedia.org/wiki/DRY).
88
112
 
89
- If you can't meet these criteria, your object is probably violating [SOLID](http://en.wikipedia.org/wiki/SOLID) principles and you should either refactor or use a non-isolated test.
113
+ If you can't meet these criteria, your object is probably violating
114
+ [SOLID](http://en.wikipedia.org/wiki/SOLID) principles and you should either
115
+ refactor or use a non-isolated test.
90
116
 
91
117
  Compatibility
92
118
  -------------
93
119
 
94
120
  Only RSpec 2 is supported. Tested on all the rubies.
95
121
 
96
- [![Build Status](https://secure.travis-ci.org/xaviershay/rspec-fire.png)](http://travis-ci.org/xaviershay/rspec-fire)
122
+ [![Build Status][build-image]][build-link]
123
+
124
+ [build-image]: https://secure.travis-ci.org/xaviershay/rspec-fire.png
125
+ [build-link]: http://travis-ci.org/xaviershay/rspec-fire
97
126
 
98
127
  Developing
99
128
  ----------
@@ -102,9 +131,10 @@ Developing
102
131
  bundle install
103
132
  bundle exec rake spec
104
133
 
105
- Patches welcome! I won't merge anything that isn't spec'ed, but I can help you out with that if you are getting stuck.
134
+ Patches welcome! I won't merge anything that isn't spec'ed, but I can help you
135
+ out with that if you are getting stuck.
106
136
 
107
- Still need to support `#stub_chain` and `#with` methods.
137
+ Still need to support `#stub_chain`.
108
138
 
109
139
  Status
110
140
  ------
data/lib/rspec/fire.rb CHANGED
@@ -1,13 +1,81 @@
1
1
  require 'rspec/mocks'
2
+ require 'delegate'
2
3
 
3
4
  module RSpec
4
5
  module Fire
6
+ module RecursiveConstMethods
7
+ def recursive_const_get object, name
8
+ name.split('::').inject(Object) {|klass,name| klass.const_get name }
9
+ end
10
+
11
+ def recursive_const_defined? object, name
12
+ !!name.split('::').inject(Object) {|klass,name|
13
+ if klass && klass.const_defined?(name)
14
+ klass.const_get name
15
+ end
16
+ }
17
+ end
18
+ end
19
+
20
+ class ShouldProxy < SimpleDelegator
21
+ include RecursiveConstMethods
22
+
23
+ AM = RSpec::Mocks::ArgumentMatchers
24
+
25
+ def initialize(doubled_class_name, method_finder, backing)
26
+ @doubled_class_name = doubled_class_name
27
+ @method_finder = method_finder
28
+ @backing = backing
29
+ super(backing)
30
+ end
31
+
32
+ def with(*args, &block)
33
+ unless AM::AnyArgsMatcher === args.first
34
+ expected_arity = if AM::NoArgsMatcher === args.first
35
+ 0
36
+ elsif args.length > 0
37
+ args.length
38
+ elsif block
39
+ block.arity
40
+ else
41
+ raise ArgumentError.new("No arguments nor block given.")
42
+ end
43
+ ensure_arity(expected_arity)
44
+ end
45
+ __getobj__.with(*args, &block)
46
+ end
47
+
48
+ protected
49
+
50
+ def ensure_arity(actual)
51
+ if recursive_const_defined?(Object, @doubled_class_name)
52
+ recursive_const_get(Object, @doubled_class_name).
53
+ send(@method_finder, sym).
54
+ should have_arity(actual)
55
+ end
56
+ end
57
+
58
+ def have_arity(actual)
59
+ RSpec::Matchers::Matcher.new(:have_arity, actual) 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
68
+ end
69
+ end
70
+ end
71
+
5
72
  class FireDouble < RSpec::Mocks::Mock
73
+ include RecursiveConstMethods
74
+
6
75
  def initialize(doubled_class, *args)
7
76
  args << {} unless Hash === args.last
8
77
 
9
78
  @__doubled_class_name = doubled_class
10
- @__checked_methods = :public_instance_methods
11
79
 
12
80
  # __declared_as copied from rspec/mocks definition of `double`
13
81
  args.last[:__declared_as] = 'FireDouble'
@@ -16,7 +84,7 @@ module RSpec
16
84
 
17
85
  def should_receive(method_name)
18
86
  ensure_implemented(method_name)
19
- super
87
+ ShouldProxy.new(@__doubled_class_name, @__method_finder, super)
20
88
  end
21
89
 
22
90
  def should_not_receive(method_name)
@@ -38,23 +106,15 @@ module RSpec
38
106
  end
39
107
  end
40
108
 
41
- def recursive_const_get object, name
42
- name.split('::').inject(Object) {|klass,name| klass.const_get name }
43
- end
44
-
45
- def recursive_const_defined? object, name
46
- !!name.split('::').inject(Object) {|klass,name|
47
- if klass && klass.const_defined?(name)
48
- klass.const_get name
49
- end
50
- }
51
- end
52
-
53
109
  def implement(expected_methods, checked_methods)
54
- RSpec::Matchers::Matcher.new(:implement, expected_methods, checked_methods) do |expected_methods, checked_methods|
110
+ RSpec::Matchers::Matcher.new(:implement,
111
+ expected_methods,
112
+ checked_methods
113
+ ) do |expected_methods, checked_methods|
55
114
  unimplemented_methods = lambda {|doubled_class|
56
115
  implemented_methods = doubled_class.send(checked_methods)
57
- expected_methods - implemented_methods.map(&:to_sym) # to_sym for non-1.9 compat
116
+ # to_sym for non-1.9 compat
117
+ expected_methods - implemented_methods.map(&:to_sym)
58
118
  }
59
119
 
60
120
  match do |doubled_class|
@@ -62,7 +122,8 @@ module RSpec
62
122
  end
63
123
 
64
124
  failure_message_for_should do |doubled_class|
65
- implemented_methods = Object.public_methods - doubled_class.send(checked_methods)
125
+ implemented_methods =
126
+ Object.public_methods - doubled_class.send(checked_methods)
66
127
  "%s does not implement:\n%s" % [
67
128
  doubled_class,
68
129
  unimplemented_methods[ doubled_class ].sort.map {|x|
@@ -74,15 +135,24 @@ module RSpec
74
135
  end
75
136
  end
76
137
 
138
+ class FireObjectDouble < FireDouble
139
+ def initialize(*args)
140
+ super
141
+ @__checked_methods = :public_instance_methods
142
+ @__method_finder = :instance_method
143
+ end
144
+ end
145
+
77
146
  class FireClassDouble < FireDouble
78
147
  def initialize(*args)
79
148
  super
80
149
  @__checked_methods = :public_methods
150
+ @__method_finder = :method
81
151
  end
82
152
  end
83
153
 
84
154
  def fire_double(*args)
85
- FireDouble.new(*args)
155
+ FireObjectDouble.new(*args)
86
156
  end
87
157
 
88
158
  def fire_class_double(*args)
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.1'
3
+ s.version = '0.2'
4
4
  s.summary = 'More resilient test doubles for RSpec.'
5
5
  s.platform = Gem::Platform::RUBY
6
6
  s.authors = ["Xavier Shay"]
@@ -1,17 +1,23 @@
1
1
  require 'spec_helper'
2
2
 
3
- class TestObject
3
+ module TestMethods
4
4
  def defined_method
5
5
  raise "Y U NO MOCK?"
6
6
  end
7
- end
8
7
 
9
- class TestClass
10
- def self.defined_method
8
+ def defined_method_one_arg(arg1)
11
9
  raise "Y U NO MOCK?"
12
10
  end
13
11
  end
14
12
 
13
+ class TestObject
14
+ include TestMethods
15
+ end
16
+
17
+ class TestClass
18
+ extend TestMethods
19
+ end
20
+
15
21
  shared_examples_for 'a fire-enhanced double method' do
16
22
  describe 'doubled class is not loaded' do
17
23
  let(:doubled_object) { fire_double("UnloadedObject") }
@@ -38,13 +44,73 @@ shared_examples_for 'a fire-enhanced double' do
38
44
  it "should not allow #{method_name}" do
39
45
  lambda {
40
46
  doubled_object.send(method_under_test, method_name)
41
- }.should fail_matching("#{method_name} does not implement", method_name)
47
+ }.should fail_matching("does not implement", method_name)
42
48
  end
43
49
  end
44
50
 
45
51
  describe '#should_receive' do
46
52
  let(:method_under_test) { :should_receive }
47
53
  it_should_behave_like 'a fire-enhanced double method'
54
+
55
+ describe '#with' do
56
+ it 'should delegate to RSpec #with method' do
57
+ doubled_object.
58
+ should_receive(:defined_method_one_arg).
59
+ with(1).
60
+ and_return(:value)
61
+ doubled_object.defined_method_one_arg(1).should == :value
62
+
63
+ doubled_object.
64
+ should_receive(:defined_method_one_arg).
65
+ with(2) {|x| x + 1 }
66
+ doubled_object.defined_method_one_arg(2).should == 3
67
+ end
68
+
69
+ it 'should not allow an arity mismatch for a method with 0 arguments' do
70
+ lambda {
71
+ doubled_object.should_receive(:defined_method).with(:x)
72
+ }.should fail_for_arguments(0, 1)
73
+ end
74
+
75
+ it 'should not allow an arity mismatch for a method with 1 argument' do
76
+ lambda {
77
+ doubled_object.should_receive(:defined_method_one_arg).with(:x, :y)
78
+ }.should fail_for_arguments(1, 2)
79
+ end
80
+
81
+ it 'should use arity of block when no arguments given' do
82
+ lambda {
83
+ doubled_object.should_receive(:defined_method_one_arg).with {|x, y| }
84
+ }.should fail_for_arguments(1, 2)
85
+ end
86
+
87
+ it 'should raise argument error when no arguments or block given' do
88
+ lambda {
89
+ doubled_object.should_receive(:defined_method_one_arg).with
90
+ }.should raise_error(ArgumentError)
91
+ end
92
+
93
+ it 'should recognize no_args param as 0 arity and fail' do
94
+ lambda {
95
+ doubled_object.should_receive(:defined_method_one_arg).with(no_args)
96
+ }.should fail_for_arguments(1, 0)
97
+ end
98
+
99
+ it 'should recognize any_args param' do
100
+ doubled_object.should_receive(:defined_method).with(any_args)
101
+ end
102
+
103
+ after do
104
+ doubled_object.rspec_reset
105
+ end
106
+
107
+ def fail_for_arguments(expected, actual)
108
+ fail_matching(
109
+ "Wrong number of arguments for defined_method",
110
+ "Expected #{expected}, got #{actual}"
111
+ )
112
+ end
113
+ end
48
114
  end
49
115
 
50
116
  describe '#should_not_receive' do
data/spec/spec_helper.rb CHANGED
@@ -5,8 +5,8 @@ RSpec.configure do |config|
5
5
 
6
6
  def fail_matching(*messages)
7
7
  raise_error(RSpec::Expectations::ExpectationNotMetError) {|error|
8
- messages.all? {|message|
9
- error.message =~ /#{Regexp.escape(message)}/
8
+ messages.each {|message|
9
+ error.message.should =~ /#{Regexp.escape(message.to_s)}/
10
10
  }
11
11
  }
12
12
  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.1'
4
+ version: '0.2'
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: 2011-09-03 00:00:00.000000000Z
12
+ date: 2011-09-04 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
16
- requirement: &2162234400 !ruby/object:Gem::Requirement
16
+ requirement: &2161554360 !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: *2162234400
24
+ version_requirements: *2161554360
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rspec
27
- requirement: &2162233580 !ruby/object:Gem::Requirement
27
+ requirement: &2161553360 !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: *2162233580
35
+ version_requirements: *2161553360
36
36
  description:
37
37
  email:
38
38
  - hello@xaviershay.com