rspec-fire 0.1 → 0.2

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