rspec-fire 0.1
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/Gemfile +3 -0
- data/HISTORY +2 -0
- data/README.md +112 -0
- data/Rakefile +7 -0
- data/lib/rspec/fire.rb +92 -0
- data/rspec-fire.gemspec +23 -0
- data/spec/fire_double_spec.rb +71 -0
- data/spec/spec_helper.rb +13 -0
- metadata +75 -0
data/Gemfile
ADDED
data/HISTORY
ADDED
data/README.md
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
rspec-fire
|
2
|
+
==========
|
3
|
+
|
4
|
+
Making your test doubles more resilient.
|
5
|
+
|
6
|
+
Once,
|
7
|
+
a younger brother came to him,
|
8
|
+
and asked,
|
9
|
+
|
10
|
+
"Father,
|
11
|
+
I have made and kept my little rule,
|
12
|
+
my fast,
|
13
|
+
my meditation and silence.
|
14
|
+
I strived to cleanse my heart of thoughts,
|
15
|
+
what more must I do?"
|
16
|
+
|
17
|
+
The elder rose up and,
|
18
|
+
stretched out his hands,
|
19
|
+
his fingers became like ten lamps ablaze.
|
20
|
+
He said,
|
21
|
+
|
22
|
+
"Why not be totally changed into fire?"
|
23
|
+
|
24
|
+
-- Desert Way, Charlie Hunter
|
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.
|
33
|
+
|
34
|
+
Usage
|
35
|
+
-----
|
36
|
+
|
37
|
+
It's a gem!
|
38
|
+
|
39
|
+
gem install rspec-fire
|
40
|
+
|
41
|
+
Bit of setup in your `spec_helper.rb`:
|
42
|
+
|
43
|
+
require 'rspec/fire'
|
44
|
+
|
45
|
+
RSpec.configure do |config|
|
46
|
+
config.include(RSpec::Fire)
|
47
|
+
end
|
48
|
+
|
49
|
+
Specify the class being doubled in your specs:
|
50
|
+
|
51
|
+
describe User, '#suspend!' do
|
52
|
+
it 'sends a notification' do
|
53
|
+
# Only this one line differs from how you write specs normally
|
54
|
+
notifier = fire_double("EmailNotifier")
|
55
|
+
|
56
|
+
notifier.should_receive(:notify).with("suspended as")
|
57
|
+
|
58
|
+
user = User.new(notifier)
|
59
|
+
user.suspend!
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
Run your specs:
|
64
|
+
|
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
|
67
|
+
|
68
|
+
Currently only method presence/absense is checked, but theoretically arity can be checked also.
|
69
|
+
|
70
|
+
Protips
|
71
|
+
-------
|
72
|
+
|
73
|
+
### Using with an existing Rails project
|
74
|
+
|
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:
|
76
|
+
|
77
|
+
rspec -rspec/spec_helper.rb spec/unit/my_spec.rb
|
78
|
+
|
79
|
+
### Doubling class methods
|
80
|
+
|
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.
|
82
|
+
|
83
|
+
### Mocking Done Right (tm)
|
84
|
+
|
85
|
+
* Only mock methods on collaborators, _not_ the class under test.
|
86
|
+
* Only mock public methods.
|
87
|
+
* Extract common mock setup to keep your specs [DRY](http://en.wikipedia.org/wiki/DRY).
|
88
|
+
|
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.
|
90
|
+
|
91
|
+
Compatibility
|
92
|
+
-------------
|
93
|
+
|
94
|
+
Only RSpec 2 is supported. Tested on all the rubies.
|
95
|
+
|
96
|
+
[](http://travis-ci.org/xaviershay/rspec-fire)
|
97
|
+
|
98
|
+
Developing
|
99
|
+
----------
|
100
|
+
|
101
|
+
git clone https://github.com/xaviershay/rspec-fire.git
|
102
|
+
bundle install
|
103
|
+
bundle exec rake spec
|
104
|
+
|
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.
|
106
|
+
|
107
|
+
Still need to support `#stub_chain` and `#with` methods.
|
108
|
+
|
109
|
+
Status
|
110
|
+
------
|
111
|
+
|
112
|
+
`rspec-fire` is pretty new and not used widely. Yet.
|
data/Rakefile
ADDED
data/lib/rspec/fire.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'rspec/mocks'
|
2
|
+
|
3
|
+
module RSpec
|
4
|
+
module Fire
|
5
|
+
class FireDouble < RSpec::Mocks::Mock
|
6
|
+
def initialize(doubled_class, *args)
|
7
|
+
args << {} unless Hash === args.last
|
8
|
+
|
9
|
+
@__doubled_class_name = doubled_class
|
10
|
+
@__checked_methods = :public_instance_methods
|
11
|
+
|
12
|
+
# __declared_as copied from rspec/mocks definition of `double`
|
13
|
+
args.last[:__declared_as] = 'FireDouble'
|
14
|
+
super(doubled_class, *args)
|
15
|
+
end
|
16
|
+
|
17
|
+
def should_receive(method_name)
|
18
|
+
ensure_implemented(method_name)
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
def should_not_receive(method_name)
|
23
|
+
ensure_implemented(method_name)
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
def stub(method_name)
|
28
|
+
ensure_implemented(method_name)
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
34
|
+
def ensure_implemented(*method_names)
|
35
|
+
if recursive_const_defined?(Object, @__doubled_class_name)
|
36
|
+
recursive_const_get(Object, @__doubled_class_name).
|
37
|
+
should implement(method_names, @__checked_methods)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
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
|
+
def implement(expected_methods, checked_methods)
|
54
|
+
RSpec::Matchers::Matcher.new(:implement, expected_methods, checked_methods) do |expected_methods, checked_methods|
|
55
|
+
unimplemented_methods = lambda {|doubled_class|
|
56
|
+
implemented_methods = doubled_class.send(checked_methods)
|
57
|
+
expected_methods - implemented_methods.map(&:to_sym) # to_sym for non-1.9 compat
|
58
|
+
}
|
59
|
+
|
60
|
+
match do |doubled_class|
|
61
|
+
unimplemented_methods[ doubled_class ].empty?
|
62
|
+
end
|
63
|
+
|
64
|
+
failure_message_for_should do |doubled_class|
|
65
|
+
implemented_methods = Object.public_methods - doubled_class.send(checked_methods)
|
66
|
+
"%s does not implement:\n%s" % [
|
67
|
+
doubled_class,
|
68
|
+
unimplemented_methods[ doubled_class ].sort.map {|x|
|
69
|
+
" #{x}"
|
70
|
+
}.join("\n")
|
71
|
+
]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class FireClassDouble < FireDouble
|
78
|
+
def initialize(*args)
|
79
|
+
super
|
80
|
+
@__checked_methods = :public_methods
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def fire_double(*args)
|
85
|
+
FireDouble.new(*args)
|
86
|
+
end
|
87
|
+
|
88
|
+
def fire_class_double(*args)
|
89
|
+
FireClassDouble.new(*args)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/rspec-fire.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'rspec-fire'
|
3
|
+
s.version = '0.1'
|
4
|
+
s.summary = 'More resilient test doubles for RSpec.'
|
5
|
+
s.platform = Gem::Platform::RUBY
|
6
|
+
s.authors = ["Xavier Shay"]
|
7
|
+
s.email = ["hello@xaviershay.com"]
|
8
|
+
s.homepage = "http://github.com/xaviershay/rspec-fire"
|
9
|
+
s.has_rdoc = false
|
10
|
+
|
11
|
+
s.require_path = 'lib'
|
12
|
+
s.files = Dir.glob("{spec,lib}/**/*.rb") +
|
13
|
+
%w(
|
14
|
+
Gemfile
|
15
|
+
README.md
|
16
|
+
HISTORY
|
17
|
+
Rakefile
|
18
|
+
rspec-fire.gemspec
|
19
|
+
)
|
20
|
+
|
21
|
+
s.add_development_dependency 'rake'
|
22
|
+
s.add_development_dependency 'rspec', '~> 2.5'
|
23
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class TestObject
|
4
|
+
def defined_method
|
5
|
+
raise "Y U NO MOCK?"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class TestClass
|
10
|
+
def self.defined_method
|
11
|
+
raise "Y U NO MOCK?"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
shared_examples_for 'a fire-enhanced double method' do
|
16
|
+
describe 'doubled class is not loaded' do
|
17
|
+
let(:doubled_object) { fire_double("UnloadedObject") }
|
18
|
+
should_allow(:undefined_method)
|
19
|
+
end
|
20
|
+
|
21
|
+
describe 'doubled class is loaded' do
|
22
|
+
should_allow(:defined_method)
|
23
|
+
should_not_allow(:undefined_method)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
shared_examples_for 'a fire-enhanced double' do
|
28
|
+
def self.should_allow(method_name)
|
29
|
+
it "should allow #{method_name}" do
|
30
|
+
lambda {
|
31
|
+
doubled_object.send(method_under_test, method_name)
|
32
|
+
}.should_not raise_error
|
33
|
+
doubled_object.rspec_reset
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.should_not_allow(method_name)
|
38
|
+
it "should not allow #{method_name}" do
|
39
|
+
lambda {
|
40
|
+
doubled_object.send(method_under_test, method_name)
|
41
|
+
}.should fail_matching("#{method_name} does not implement", method_name)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe '#should_receive' do
|
46
|
+
let(:method_under_test) { :should_receive }
|
47
|
+
it_should_behave_like 'a fire-enhanced double method'
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '#should_not_receive' do
|
51
|
+
let(:method_under_test) { :should_not_receive }
|
52
|
+
it_should_behave_like 'a fire-enhanced double method'
|
53
|
+
end
|
54
|
+
|
55
|
+
describe '#stub' do
|
56
|
+
let(:method_under_test) { :stub }
|
57
|
+
it_should_behave_like 'a fire-enhanced double method'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe '#fire_double' do
|
62
|
+
let(:doubled_object) { fire_double("TestObject") }
|
63
|
+
|
64
|
+
it_should_behave_like 'a fire-enhanced double'
|
65
|
+
end
|
66
|
+
|
67
|
+
describe '#fire_class_double' do
|
68
|
+
let(:doubled_object) { fire_class_double("TestClass") }
|
69
|
+
|
70
|
+
it_should_behave_like 'a fire-enhanced double'
|
71
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rspec/fire'
|
2
|
+
|
3
|
+
RSpec.configure do |config|
|
4
|
+
config.include(RSpec::Fire)
|
5
|
+
|
6
|
+
def fail_matching(*messages)
|
7
|
+
raise_error(RSpec::Expectations::ExpectationNotMetError) {|error|
|
8
|
+
messages.all? {|message|
|
9
|
+
error.message =~ /#{Regexp.escape(message)}/
|
10
|
+
}
|
11
|
+
}
|
12
|
+
end
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rspec-fire
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Xavier Shay
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-09-03 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: &2162234400 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2162234400
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rspec
|
27
|
+
requirement: &2162233580 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2.5'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2162233580
|
36
|
+
description:
|
37
|
+
email:
|
38
|
+
- hello@xaviershay.com
|
39
|
+
executables: []
|
40
|
+
extensions: []
|
41
|
+
extra_rdoc_files: []
|
42
|
+
files:
|
43
|
+
- spec/fire_double_spec.rb
|
44
|
+
- spec/spec_helper.rb
|
45
|
+
- lib/rspec/fire.rb
|
46
|
+
- Gemfile
|
47
|
+
- README.md
|
48
|
+
- HISTORY
|
49
|
+
- Rakefile
|
50
|
+
- rspec-fire.gemspec
|
51
|
+
homepage: http://github.com/xaviershay/rspec-fire
|
52
|
+
licenses: []
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options: []
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
requirements: []
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 1.8.10
|
72
|
+
signing_key:
|
73
|
+
specification_version: 3
|
74
|
+
summary: More resilient test doubles for RSpec.
|
75
|
+
test_files: []
|