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 +3 -0
- data/README.md +47 -17
- data/lib/rspec/fire.rb +88 -18
- data/rspec-fire.gemspec +1 -1
- data/spec/fire_double_spec.rb +71 -5
- data/spec/spec_helper.rb +2 -2
- metadata +6 -6
data/HISTORY
CHANGED
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
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
66
|
-
rspec
|
80
|
+
# Isolated, will pass always
|
81
|
+
rspec spec/user_spec.rb
|
67
82
|
|
68
|
-
|
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`.
|
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
|
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
|
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
|
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]
|
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
|
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
|
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,
|
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
|
-
|
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
|
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
|
-
|
155
|
+
FireObjectDouble.new(*args)
|
86
156
|
end
|
87
157
|
|
88
158
|
def fire_class_double(*args)
|
data/rspec-fire.gemspec
CHANGED
data/spec/fire_double_spec.rb
CHANGED
@@ -1,17 +1,23 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
|
3
|
+
module TestMethods
|
4
4
|
def defined_method
|
5
5
|
raise "Y U NO MOCK?"
|
6
6
|
end
|
7
|
-
end
|
8
7
|
|
9
|
-
|
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("
|
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.
|
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.
|
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-
|
12
|
+
date: 2011-09-04 00:00:00.000000000Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|
16
|
-
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: *
|
24
|
+
version_requirements: *2161554360
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: rspec
|
27
|
-
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: *
|
35
|
+
version_requirements: *2161553360
|
36
36
|
description:
|
37
37
|
email:
|
38
38
|
- hello@xaviershay.com
|