rspec-fire 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/HISTORY ADDED
@@ -0,0 +1,2 @@
1
+ 0.1 - 3 September 2011
2
+ * Initial release
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
@@ -0,0 +1,7 @@
1
+ require 'rspec/core/rake_task'
2
+ RSpec::Core::RakeTask.new(:spec) do |opts|
3
+ opts.rspec_opts = '--format documentation'
4
+ end
5
+
6
+ desc 'Default: run specs.'
7
+ task :default => :spec
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
@@ -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
@@ -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: []