rspec-subject_call 1.0.0

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/.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: