rspec-subject_call 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format d
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ #gem 'pry-debugger'
4
+
5
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,14 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rspec-subject_call (1.0.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+
10
+ PLATFORMS
11
+ ruby
12
+
13
+ DEPENDENCIES
14
+ rspec-subject_call!
data/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # RSpec Subject Call
2
+
3
+ This gem permits succinct one liner syntax in situations that currently require
4
+ more than one line of code. It's for impatient people who use RSpec and like
5
+ saving keystrokes.
6
+
7
+ ## The Situation
8
+
9
+ There are different types of methods I sometimes need to test. [[1]]
10
+
11
+ A **query** performs a calculation and returns a value, like `sum`.
12
+
13
+ A **command** does one thing. Often it changes one piece of state.
14
+ e.g. `x=` or `print_document`.
15
+
16
+ A method may also have a number of **side effects** which you *may* wish to
17
+ make assertions about, such as caching, keeping counts or raising exceptions.
18
+
19
+ To illustrate, here is a class that has all these types of methods.
20
+
21
+ ```ruby
22
+ class A
23
+ attr :x
24
+
25
+ def initialize(args = {})
26
+ @b = args[:b]
27
+ @x = 0
28
+ end
29
+
30
+ def query
31
+ 1
32
+ end
33
+
34
+ def command
35
+ @x = 1
36
+ end
37
+
38
+ def query_command
39
+ @x = 1
40
+ 1
41
+ end
42
+
43
+ def query_command_side_effect
44
+ @b.command
45
+ @x = 1
46
+ 1
47
+ end
48
+ end
49
+
50
+ class B
51
+ def command
52
+ end
53
+ end
54
+ ```
55
+
56
+ Note that `query_command_side_effect` does ALL the things. So that is a good
57
+ method to try testing elegantly. If we can test that elegantly, we can test
58
+ anything elegantly. Right?
59
+
60
+ With vanilla RSpec, I might test this method like this:
61
+
62
+ ```ruby
63
+ describe A do
64
+ let(:a) { A.new(b: b) }
65
+ let(:b) { double('b').as_null_object }
66
+
67
+ describe '#query_command_side_effect' do
68
+ it 'should return 1' do
69
+ a.query_command_side_effect.should == 1
70
+ end
71
+
72
+ it 'should update x' do
73
+ expect { a.query_command_side_effect }.to change(a, :x)
74
+ end
75
+
76
+ it 'should call command on b' do
77
+ b.should_receive(:command).once
78
+ a.query_command_side_effect
79
+ end
80
+ end
81
+ end
82
+ ```
83
+
84
+ But I feel as though this is more typing that I would like and could be DRYed
85
+ up. For example, `a.query_command_side_effect` is repeated three times.
86
+
87
+ ## The Subject Call Solution
88
+
89
+ This is how I would like to test this tricky method:
90
+
91
+ ```ruby
92
+ require 'rspec/subject_call'
93
+
94
+ describe A do
95
+ let(:a) { A.new(b: b) }
96
+ let(:b) { double('b').as_null_object }
97
+
98
+ describe '#query_command_side_effect' do
99
+ subject { a.query_command_side_effect }
100
+ it { should == 1 }
101
+ call { should change(a, :x) }
102
+ call { should meet_expectations { b.should_receive(:command) } }
103
+ end
104
+ end
105
+ ```
106
+
107
+ In the above example, `it` is short for the return value of the method under
108
+ test, and `call` is a lambda containing the subject, suitable for use with
109
+ `change` and `raise_error` and any other matcher that works with lambdas,
110
+ already packaged up for you and ready to use.
111
+
112
+ With this gem, the above example passes.
113
+
114
+
115
+ [1]: http://en.wikipedia.org/wiki/Command%E2%80%93query_separation "Command-Query Separation"
@@ -0,0 +1,28 @@
1
+ module RSpec
2
+ module SubjectCall
3
+ module Matchers
4
+ # A general purpose matcher that inverts order of operations
5
+ # allowing for one liners involving mock expectations.
6
+ class MeetExpectationsMatcher
7
+ def initialize(&block)
8
+ @should_receives = block
9
+ end
10
+
11
+ def matches?(subject)
12
+ @should_receives.call # e.g. x.should_receive(:y)
13
+ subject.call # execute the subject (assumed to be a Proc)
14
+ end
15
+
16
+ def description
17
+ 'met expectations'
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ module Matchers
24
+ def meet_expectations(&block)
25
+ ::RSpec::SubjectCall::Matchers::MeetExpectationsMatcher.new(&block)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ require 'rspec/matchers/pretty'
2
+ require 'rspec/matchers/built_in'
3
+
4
+ module RSpec
5
+ module SubjectCall
6
+ module Matchers
7
+ class ReturnValueMatcher < RSpec::Matchers::BuiltIn::Eq
8
+ def matches?(subject)
9
+ @actual = subject.call
10
+ end
11
+
12
+ def description
13
+ "return #{expected.inspect}"
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ module Matchers
20
+ def return_value(expected)
21
+ ::RSpec::SubjectCall::Matchers::ReturnValueMatcher.new(expected)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ module RSpec
2
+ module SubjectCall
3
+ VERSION = '1.0.0'
4
+ end
5
+ end
@@ -0,0 +1,99 @@
1
+ require 'rspec/subject_call/matchers/meet_expectations_matcher'
2
+ require 'rspec/subject_call/matchers/return_value_matcher'
3
+
4
+ module RSpec
5
+ module SubjectCall
6
+ module ExampleGroupClassMethods
7
+ # Define a method +subject+ for use inside examples, and also a method
8
+ # +call+ which returns a lambda containing the subject, suitable for use
9
+ # with matchers that take lambdas.
10
+ #
11
+ # e.g.
12
+ #
13
+ # subject { obj.my_method }
14
+ #
15
+ # it { should == some_result }
16
+ #
17
+ # it 'should change something, should syntax' do
18
+ # call.should change{something}
19
+ # end
20
+ #
21
+ # it 'should change something, expect syntax' do
22
+ # expect(call).to change{something}
23
+ # end
24
+ #
25
+ def subject(name=nil, &block)
26
+ if block
27
+ define_method(:call_subject) do
28
+ instance_eval(&block)
29
+ end
30
+ define_method(:call) do
31
+ lambda { call_subject }
32
+ end
33
+ end
34
+
35
+ # Do the things subject normally does.
36
+ super(name, &block)
37
+ end
38
+
39
+ # Allow for syntax similar to +its+:
40
+ #
41
+ # its(:my_method) { should == 1 }
42
+ # calling(:my_method) { should change{something} }
43
+ #
44
+ def calling(method_name, &block)
45
+ describe(method_name) do
46
+ let(:__its_subject) do
47
+ method_chain = method_name.to_s.split('.')
48
+ lambda do
49
+ method_chain.inject(subject) do |inner_subject, attr|
50
+ inner_subject.send(attr)
51
+ end
52
+ end
53
+ end
54
+
55
+ def should(matcher=nil, message=nil)
56
+ RSpec::Expectations::PositiveExpectationHandler.handle_matcher(__its_subject, matcher, message)
57
+ end
58
+
59
+ def should_not(matcher=nil, message=nil)
60
+ RSpec::Expectations::NegativeExpectationHandler.handle_matcher(__its_subject, matcher, message)
61
+ end
62
+
63
+ example(&block)
64
+ end
65
+ end
66
+
67
+ # Like +it+ but sets the implicit subject to the lambda you supplied when
68
+ # defining the subject, so that you can use it with matchers that take
69
+ # blocks like +change+:
70
+ #
71
+ # call { should change{something} }
72
+ #
73
+ def call(desc=nil, *args, &block)
74
+ # Create a new example, where the subject is set to the subject block,
75
+ # as opposed to its return value.
76
+ example do
77
+ self.class.class_eval do
78
+ define_method(:subject) do
79
+ if defined?(@_subject_call)
80
+ @_subject_call
81
+ else
82
+ @_subject_call = call
83
+ end
84
+ end
85
+ end
86
+ instance_eval(&block)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ module RSpec
94
+ module Core
95
+ class ExampleGroup
96
+ extend ::RSpec::SubjectCall::ExampleGroupClassMethods
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,17 @@
1
+ require File.expand_path('lib/rspec/subject_call/version', File.dirname(__FILE__))
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'rspec-subject_call'
5
+ s.version = RSpec::SubjectCall::VERSION
6
+ s.platform = Gem::Platform::RUBY
7
+ s.authors = [ 'Gregory McIntyre' ]
8
+ s.email = [ 'greg@gregorymcintyre.com' ]
9
+ s.homepage = 'http://github.com/rails-oceania/rspec-subject_call'
10
+ s.description = 'Enable reuse of the subject for use with expect..to..change and my_double.should_receive'
11
+ s.summary = "rspec-subject_call-#{s.version}"
12
+ s.required_rubygems_version = '> 1.3.6'
13
+
14
+ s.files = `git ls-files`.split($\)
15
+ s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
16
+ s.require_path = 'lib'
17
+ end
@@ -0,0 +1,66 @@
1
+ class A
2
+ attr :x
3
+
4
+ def initialize(args = {})
5
+ @b = args[:b]
6
+ @x = 0
7
+ end
8
+
9
+ def query
10
+ 1
11
+ end
12
+
13
+ def command
14
+ @x = 1
15
+ end
16
+
17
+ def query_command
18
+ @x = 1
19
+ 1
20
+ end
21
+
22
+ def query_command_side_effect
23
+ @b.command
24
+ @x = 1
25
+ 1
26
+ end
27
+ end
28
+
29
+ class B
30
+ def command
31
+ end
32
+ end
33
+
34
+ describe A do
35
+ let(:a) { A.new(b: b) }
36
+ let(:b) { double('b').as_null_object }
37
+
38
+ describe '#query_command_side_effect' do
39
+ it 'should return 1' do
40
+ a.query_command_side_effect.should == 1
41
+ end
42
+
43
+ it 'should update x' do
44
+ expect { a.query_command_side_effect }.to change(a, :x)
45
+ end
46
+
47
+ it 'should call command on b' do
48
+ b.should_receive(:command).once
49
+ a.query_command_side_effect
50
+ end
51
+ end
52
+ end
53
+
54
+ require 'rspec/subject_call'
55
+
56
+ describe A do
57
+ let(:a) { A.new(b: b) }
58
+ let(:b) { double('b').as_null_object }
59
+
60
+ describe '#query_command_side_effect' do
61
+ subject { a.query_command_side_effect }
62
+ it { should == 1 }
63
+ call { should change(a, :x) }
64
+ call { should meet_expectations { b.should_receive(:command) } }
65
+ end
66
+ end
@@ -0,0 +1,127 @@
1
+ class A
2
+ attr_reader :counter
3
+
4
+ def initialize(args = {})
5
+ @b = args[:b]
6
+ @counter = 0
7
+ end
8
+
9
+ def query
10
+ 1
11
+ end
12
+
13
+ def command
14
+ @b.command
15
+ end
16
+
17
+ def query_command
18
+ @b.command
19
+ 1
20
+ end
21
+
22
+ def query_command_side_effect
23
+ @b.command
24
+ @counter += 1
25
+ 1
26
+ end
27
+ end
28
+
29
+ class B
30
+ def command
31
+ end
32
+ end
33
+
34
+ # ----------------------------------------------------------------------
35
+
36
+ require 'rspec/subject_call'
37
+
38
+ describe A do
39
+ subject(:a) { A.new(b: b) }
40
+ let(:b) { double('b').as_null_object }
41
+
42
+ describe '#query' do
43
+ it 'should return 1' do
44
+ a.query.should == 1
45
+ end
46
+ end
47
+
48
+ describe '#command' do
49
+ it 'should call command on b' do
50
+ b.should_receive(:command).once
51
+ a.command
52
+ end
53
+ end
54
+
55
+ describe '#query_command' do
56
+ it 'should return 1' do
57
+ a.query_command.should == 1
58
+ end
59
+
60
+ it 'should call command on b' do
61
+ b.should_receive(:command).once
62
+ a.query_command
63
+ end
64
+ end
65
+
66
+ describe '#query_command_side_effect - vanilla' do
67
+ it 'should return 1' do
68
+ a.query_command_side_effect.should == 1
69
+ end
70
+
71
+ it 'should call command on b' do
72
+ b.should_receive(:command).once
73
+ a.query_command_side_effect
74
+ end
75
+
76
+ it 'should increment counter' do
77
+ expect { a.query_command_side_effect }.to change(a, :counter).by(1)
78
+ end
79
+ end
80
+
81
+ describe '#query_command_side_effect - subject_call with custom doc strings' do
82
+ subject { a.query_command_side_effect }
83
+
84
+ it 'should return 1' do
85
+ subject.should == 1
86
+ end
87
+
88
+ it 'should call b.command' do
89
+ b.should_receive(:command)
90
+ call_subject
91
+ end
92
+
93
+ it 'should increment counter by 1' do
94
+ call { should change(a, :counter).by(1) }
95
+ end
96
+ end
97
+
98
+ describe '#query_command_side_effect - subject_call with one liners' do
99
+ subject { a.query_command_side_effect }
100
+ it { should == 1 }
101
+ call { should change(a, :counter).by(1) }
102
+ call { should meet_expectations { b.should_receive(:command) } }
103
+ end
104
+
105
+ describe '#query_command_side_effect - subject_call, its style' do
106
+ subject { a }
107
+ its(:query_command_side_effect) { should == 1 }
108
+ calling(:query_command_side_effect) { should change(a, :counter).by(1) }
109
+ calling(:query_command_side_effect) { should meet_expectations { b.should_receive(:command) } }
110
+ end
111
+
112
+ describe '#query_command_side_effect - expect syntax' do
113
+ subject { a.query_command_side_effect }
114
+
115
+ it 'should return 1' do
116
+ expect(subject).to eq(1)
117
+ end
118
+
119
+ it 'should call b.command' do
120
+ expect(call).to meet_expectations { b.should_receive(:command) }
121
+ end
122
+
123
+ it 'should increment counter by 1' do
124
+ expect(call).to change(a, :counter).by(1)
125
+ end
126
+ end
127
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec-subject_call
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Gregory McIntyre
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-08-04 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Enable reuse of the subject for use with expect..to..change and my_double.should_receive
15
+ email:
16
+ - greg@gregorymcintyre.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - .rspec
22
+ - Gemfile
23
+ - Gemfile.lock
24
+ - README.md
25
+ - lib/rspec/subject_call.rb
26
+ - lib/rspec/subject_call/matchers/meet_expectations_matcher.rb
27
+ - lib/rspec/subject_call/matchers/return_value_matcher.rb
28
+ - lib/rspec/subject_call/version.rb
29
+ - rspec-subject_call.gemspec
30
+ - spec/readme_example_spec.rb
31
+ - spec/rspec_subject_call_spec.rb
32
+ homepage: http://github.com/rails-oceania/rspec-subject_call
33
+ licenses: []
34
+ post_install_message:
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ none: false
46
+ requirements:
47
+ - - ! '>'
48
+ - !ruby/object:Gem::Version
49
+ version: 1.3.6
50
+ requirements: []
51
+ rubyforge_project:
52
+ rubygems_version: 1.8.24
53
+ signing_key:
54
+ specification_version: 3
55
+ summary: rspec-subject_call-1.0.0
56
+ test_files: []
57
+ has_rdoc: