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 +2 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +14 -0
- data/README.md +115 -0
- data/lib/rspec/subject_call/matchers/meet_expectations_matcher.rb +28 -0
- data/lib/rspec/subject_call/matchers/return_value_matcher.rb +24 -0
- data/lib/rspec/subject_call/version.rb +5 -0
- data/lib/rspec/subject_call.rb +99 -0
- data/rspec-subject_call.gemspec +17 -0
- data/spec/readme_example_spec.rb +66 -0
- data/spec/rspec_subject_call_spec.rb +127 -0
- metadata +57 -0
data/.rspec
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
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,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:
|