rspec-fire 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://secure.travis-ci.org/xaviershay/rspec-fire.png)](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: []
|