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 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: []