surrogate 0.1.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/.gitignore +4 -0
- data/.rvmrc +7 -0
- data/Gemfile +4 -0
- data/Rakefile +1 -0
- data/Readme.md +54 -0
- data/lib/surrogate/api_comparer.rb +126 -0
- data/lib/surrogate/endower.rb +108 -0
- data/lib/surrogate/hatchery.rb +48 -0
- data/lib/surrogate/hatchling.rb +78 -0
- data/lib/surrogate/method_queue.rb +14 -0
- data/lib/surrogate/options.rb +28 -0
- data/lib/surrogate/rspec/api_method_matchers.rb +246 -0
- data/lib/surrogate/rspec/substitutability_matchers.rb +24 -0
- data/lib/surrogate/rspec.rb +2 -0
- data/lib/surrogate/version.rb +3 -0
- data/lib/surrogate.rb +17 -0
- data/spec/acceptance_spec.rb +131 -0
- data/spec/defining_api_methods_spec.rb +295 -0
- data/spec/rspec/have_been_asked_for_its_spec.rb +143 -0
- data/spec/rspec/have_been_initialized_with_spec.rb +29 -0
- data/spec/rspec/have_been_told_to_spec.rb +133 -0
- data/spec/rspec/messages_spec.rb +33 -0
- data/spec/rspec/substitute_for_spec.rb +73 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/unit/api_comparer_spec.rb +138 -0
- data/surrogate.gemspec +24 -0
- metadata +92 -0
@@ -0,0 +1,246 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
|
4
|
+
class Surrogate
|
5
|
+
module RSpec
|
6
|
+
module MessagesFor
|
7
|
+
|
8
|
+
MESSAGES = {
|
9
|
+
verb: {
|
10
|
+
should: {
|
11
|
+
default: "was never told to <%= subject %>",
|
12
|
+
with: "should have been told to <%= subject %> with <%= inspect_arguments expected_arguments %>, but <%= actual_invocation %>",
|
13
|
+
times: "should have been told to <%= subject %> <%= times_msg expected_times_invoked %> but was told to <%= subject %> <%= times_msg times_invoked %>",
|
14
|
+
with_times: "should have been told to <%= subject %> <%= times_msg expected_times_invoked %> with <%= inspect_arguments expected_arguments %>, but <%= actual_invocation %>",
|
15
|
+
},
|
16
|
+
should_not: {
|
17
|
+
default: "shouldn't have been told to <%= subject %>, but was told to <%= subject %> <%= times_msg times_invoked %>",
|
18
|
+
with: "should not have been told to <%= subject %> with <%= inspect_arguments expected_arguments %>, but <%= actual_invocation %>",
|
19
|
+
times: "shouldn't have been told to <%= subject %> <%= times_msg expected_times_invoked %>, but was",
|
20
|
+
with_times: "should not have been told to <%= subject %> <%= times_msg expected_times_invoked %> with <%= inspect_arguments expected_arguments %>, but <%= actual_invocation %>",
|
21
|
+
},
|
22
|
+
other: {
|
23
|
+
not_invoked: "was never told to",
|
24
|
+
invoked_description: "got it",
|
25
|
+
},
|
26
|
+
},
|
27
|
+
noun: {
|
28
|
+
should: {
|
29
|
+
default: "was never asked for its <%= subject %>",
|
30
|
+
with: "should have been asked for its <%= subject %> with <%= inspect_arguments expected_arguments %>, but <%= actual_invocation %>",
|
31
|
+
times: "should have been asked for its <%= subject %> <%= times_msg expected_times_invoked %>, but was asked <%= times_msg times_invoked %>",
|
32
|
+
with_times: "should have been asked for its <%= subject %> <%= times_msg expected_times_invoked %> with <%= inspect_arguments expected_arguments %>, but <%= actual_invocation %>",
|
33
|
+
},
|
34
|
+
should_not: {
|
35
|
+
default: "shouldn't have been asked for its <%= subject %>, but was asked <%= times_msg times_invoked %>",
|
36
|
+
with: "should not have been asked for its <%= subject %> with <%= inspect_arguments expected_arguments %>, but <%= actual_invocation %>",
|
37
|
+
times: "shouldn't have been asked for its <%= subject %> <%= times_msg expected_times_invoked %>, but was",
|
38
|
+
with_times: "should not have been asked for its <%= subject %> <%= times_msg expected_times_invoked %> with <%= inspect_arguments expected_arguments %>, but <%= actual_invocation %>",
|
39
|
+
},
|
40
|
+
other: {
|
41
|
+
not_invoked: "was never asked",
|
42
|
+
invoked_description: "was asked",
|
43
|
+
},
|
44
|
+
},
|
45
|
+
}
|
46
|
+
|
47
|
+
def message_for(language_type, message_category, message_type, binding)
|
48
|
+
message = MESSAGES[language_type][message_category].fetch(message_type)
|
49
|
+
ERB.new(message).result(binding)
|
50
|
+
end
|
51
|
+
|
52
|
+
def inspect_arguments(arguments)
|
53
|
+
inspected_arguments = arguments.map { |argument| MessagesFor.inspect_argument argument }
|
54
|
+
inspected_arguments << 'no_args' if inspected_arguments.empty?
|
55
|
+
%Q(`#{inspected_arguments.join ", "}')
|
56
|
+
end
|
57
|
+
|
58
|
+
def inspect_argument(to_inspect)
|
59
|
+
if to_inspect.kind_of? ::RSpec::Mocks::ArgumentMatchers::NoArgsMatcher
|
60
|
+
"no_args"
|
61
|
+
else
|
62
|
+
to_inspect.inspect
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
extend self
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
class Handler < Struct.new(:subject, :language_type)
|
71
|
+
attr_accessor :instance, :message_type
|
72
|
+
|
73
|
+
def message_for(message_category, message_type)
|
74
|
+
MessagesFor.message_for(language_type, message_category, message_type, binding)
|
75
|
+
end
|
76
|
+
|
77
|
+
def inspect_arguments(args)
|
78
|
+
MessagesFor.inspect_arguments args
|
79
|
+
end
|
80
|
+
|
81
|
+
def message_type
|
82
|
+
@message_type || :default
|
83
|
+
end
|
84
|
+
|
85
|
+
def invocations
|
86
|
+
instance.invocations(subject)
|
87
|
+
end
|
88
|
+
|
89
|
+
def times_invoked
|
90
|
+
invocations.size
|
91
|
+
end
|
92
|
+
|
93
|
+
def match?
|
94
|
+
times_invoked > 0
|
95
|
+
end
|
96
|
+
|
97
|
+
def times_msg(n)
|
98
|
+
"#{n} time#{'s' unless n == 1}"
|
99
|
+
end
|
100
|
+
|
101
|
+
def failure_message_for_should
|
102
|
+
message_for :should, message_type
|
103
|
+
end
|
104
|
+
|
105
|
+
def failure_message_for_should_not
|
106
|
+
message_for :should_not, message_type
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
module MatchWithArguments
|
112
|
+
def self.extended(klass)
|
113
|
+
klass.message_type = :with
|
114
|
+
end
|
115
|
+
|
116
|
+
attr_accessor :expected_arguments
|
117
|
+
|
118
|
+
def match? # eventually this will need to get a lot smarter
|
119
|
+
if expected_arguments.size == 1 && expected_arguments.first.kind_of?(::RSpec::Mocks::ArgumentMatchers::NoArgsMatcher)
|
120
|
+
invocations.include? []
|
121
|
+
else
|
122
|
+
invocations.include? expected_arguments
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def actual_invocation
|
127
|
+
return message_for :other, :not_invoked if times_invoked.zero?
|
128
|
+
inspected_invocations = invocations.map { |invocation| inspect_arguments invocation }
|
129
|
+
"got #{inspected_invocations.join ', '}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
module MatchNumTimes
|
135
|
+
def self.extended(klass)
|
136
|
+
klass.message_type = :times
|
137
|
+
end
|
138
|
+
|
139
|
+
attr_accessor :expected_times_invoked
|
140
|
+
|
141
|
+
def match?
|
142
|
+
expected_times_invoked == times_invoked
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
module MatchNumTimesWith
|
148
|
+
def self.extended(klass)
|
149
|
+
klass.message_type = :with_times
|
150
|
+
end
|
151
|
+
|
152
|
+
attr_accessor :expected_times_invoked, :expected_arguments
|
153
|
+
|
154
|
+
def times_invoked_with_expected_args
|
155
|
+
invocations.select { |invocation| invocation == expected_arguments }.size
|
156
|
+
end
|
157
|
+
|
158
|
+
def match?
|
159
|
+
times_invoked_with_expected_args == expected_times_invoked
|
160
|
+
end
|
161
|
+
|
162
|
+
def actual_invocation
|
163
|
+
return message_for :other, :not_invoked if times_invoked.zero?
|
164
|
+
"#{message_for :other, :invoked_description} #{times_msg times_invoked_with_expected_args}"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
|
170
|
+
|
171
|
+
|
172
|
+
# have_been_told_to
|
173
|
+
::RSpec::Matchers.define :have_been_told_to do |verb|
|
174
|
+
use_case = Handler.new verb, :verb
|
175
|
+
|
176
|
+
match do |mocked_instance|
|
177
|
+
use_case.instance = mocked_instance
|
178
|
+
use_case.match?
|
179
|
+
end
|
180
|
+
|
181
|
+
chain :times do |number|
|
182
|
+
use_case.extend (use_case.kind_of?(MatchWithArguments) ? MatchNumTimesWith : MatchNumTimes)
|
183
|
+
use_case.expected_times_invoked = number
|
184
|
+
end
|
185
|
+
|
186
|
+
chain :with do |*arguments|
|
187
|
+
use_case.extend (use_case.kind_of?(MatchNumTimes) ? MatchNumTimesWith : MatchWithArguments)
|
188
|
+
use_case.expected_arguments = arguments
|
189
|
+
end
|
190
|
+
|
191
|
+
failure_message_for_should { use_case.failure_message_for_should }
|
192
|
+
failure_message_for_should_not { use_case.failure_message_for_should_not }
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
# have_been_asked_for_its
|
197
|
+
::RSpec::Matchers.define :have_been_asked_for_its do |noun|
|
198
|
+
use_case = Handler.new noun, :noun
|
199
|
+
|
200
|
+
match do |mocked_instance|
|
201
|
+
use_case.instance = mocked_instance
|
202
|
+
use_case.match?
|
203
|
+
end
|
204
|
+
|
205
|
+
chain :times do |number|
|
206
|
+
use_case.extend (use_case.kind_of?(MatchWithArguments) ? MatchNumTimesWith : MatchNumTimes)
|
207
|
+
use_case.expected_times_invoked = number
|
208
|
+
end
|
209
|
+
|
210
|
+
chain :with do |*arguments|
|
211
|
+
use_case.extend (use_case.kind_of?(MatchNumTimes) ? MatchNumTimesWith : MatchWithArguments)
|
212
|
+
use_case.expected_arguments = arguments
|
213
|
+
end
|
214
|
+
|
215
|
+
failure_message_for_should { use_case.failure_message_for_should }
|
216
|
+
failure_message_for_should_not { use_case.failure_message_for_should_not }
|
217
|
+
end
|
218
|
+
|
219
|
+
|
220
|
+
# have_been_initialized_with
|
221
|
+
::RSpec::Matchers.define :have_been_initialized_with do |*init_args|
|
222
|
+
use_case = Handler.new :initialize, :verb
|
223
|
+
use_case.extend MatchWithArguments
|
224
|
+
use_case.expected_arguments = init_args
|
225
|
+
|
226
|
+
match do |mocked_instance|
|
227
|
+
use_case.instance = mocked_instance
|
228
|
+
use_case.match?
|
229
|
+
end
|
230
|
+
|
231
|
+
chain :nothing do
|
232
|
+
use_case.expected_arguments = nothing
|
233
|
+
end
|
234
|
+
|
235
|
+
failure_message_for_should do
|
236
|
+
use_case.failure_message_for_should
|
237
|
+
end
|
238
|
+
|
239
|
+
failure_message_for_should_not do
|
240
|
+
use_case.failure_message_for_should_not
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Surrogate
|
2
|
+
module RSpec
|
3
|
+
module MessagesFor
|
4
|
+
::RSpec::Matchers.define :substitute_for do |original_class|
|
5
|
+
|
6
|
+
comparison = nil
|
7
|
+
|
8
|
+
match do |mocked_class|
|
9
|
+
comparison = ApiComparer.new(mocked_class, original_class).compare
|
10
|
+
(comparison[:instance].values + comparison[:class].values).inject(:+).empty?
|
11
|
+
end
|
12
|
+
|
13
|
+
failure_message_for_should do
|
14
|
+
"Should have been substitute, but found these differences #{comparison.inspect}"
|
15
|
+
end
|
16
|
+
|
17
|
+
failure_message_for_should_not do
|
18
|
+
"Should not have been substitute, but was"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
data/lib/surrogate.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'surrogate/version'
|
2
|
+
require 'surrogate/hatchling'
|
3
|
+
require 'surrogate/hatchery'
|
4
|
+
require 'surrogate/options'
|
5
|
+
require 'surrogate/method_queue'
|
6
|
+
require 'surrogate/endower'
|
7
|
+
require 'surrogate/api_comparer'
|
8
|
+
|
9
|
+
class Surrogate
|
10
|
+
UnpreparedMethodError = Class.new StandardError
|
11
|
+
|
12
|
+
# TODO: Find a new name that isn't "playlist"
|
13
|
+
def self.endow(klass, &playlist)
|
14
|
+
Endower.endow klass, &playlist
|
15
|
+
klass
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Surrogate do
|
4
|
+
it 'passes this acceptance spec' do
|
5
|
+
module Mock
|
6
|
+
class User
|
7
|
+
|
8
|
+
# things sung inside the block are sungd to User's singleton class (ie User.find)
|
9
|
+
Surrogate.endow self do
|
10
|
+
|
11
|
+
# the block is used as a default value unless overridden by the spec
|
12
|
+
define :find do |id|
|
13
|
+
new id
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
# things sung outside the block are sung at User (ie user.id)
|
19
|
+
|
20
|
+
define :initialize do |id|
|
21
|
+
@id = id # can set the @id ivar to give the #id method a default
|
22
|
+
@phone_numbers = []
|
23
|
+
end
|
24
|
+
|
25
|
+
define :id
|
26
|
+
define(:name) { 'Josh' }
|
27
|
+
define :address
|
28
|
+
|
29
|
+
define :phone_numbers
|
30
|
+
|
31
|
+
define :add_phone_number do |area_code, number|
|
32
|
+
@phone_numbers << [area_code, number]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
# don't affect the real user class
|
39
|
+
user_class = Mock::User.reprise
|
40
|
+
|
41
|
+
|
42
|
+
# ===== set a default =====
|
43
|
+
user_class.will_find :user1
|
44
|
+
user_class.find(1).should == :user1
|
45
|
+
user_class.find(2).should == :user1
|
46
|
+
|
47
|
+
# set a queue of default values
|
48
|
+
user_class.will_find :user1, :user2, :user3 # set three overrides
|
49
|
+
user_class.find(11).should == :user1 # first override
|
50
|
+
user_class.find(22).should == :user2 # second override
|
51
|
+
user_class.find(33).should == :user3 # third override
|
52
|
+
user_class.find(44).should be_a_kind_of Mock::User # back to default block
|
53
|
+
# might also be nice to provide a way to raise an error
|
54
|
+
|
55
|
+
# tracking invocations
|
56
|
+
user_class = Mock::User.reprise
|
57
|
+
user_class.should_not have_been_told_to :find
|
58
|
+
user_class.find 12
|
59
|
+
user_class.find 12
|
60
|
+
user_class.find 23
|
61
|
+
user_class.should have_been_told_to(:find).times(3)
|
62
|
+
user_class.should have_been_told_to(:find).with(12)
|
63
|
+
user_class.should have_been_told_to(:find).with(12).times(2)
|
64
|
+
# user_class.should have_been_told_to(:find).with(22).and_with(33) # not sure if we really care about this (i.e. probably this will come in a later release if we like the lib)
|
65
|
+
# user_class.should have_been_told_to(:find).with(11).before(22) # not sure if we really care about this
|
66
|
+
|
67
|
+
expect { user_class.should have_been_told_to(:find).with(123123123) }.to raise_error RSpec::Expectations::ExpectationNotMetError
|
68
|
+
|
69
|
+
|
70
|
+
# ===== on the instances =====
|
71
|
+
user = user_class.find 123
|
72
|
+
|
73
|
+
# tracking initialization args
|
74
|
+
user.should have_been_initialized_with 123
|
75
|
+
|
76
|
+
# tracking invocations (these are just synonyms to try and fit the language you would want to use in a spec)
|
77
|
+
# user.should_not have_been_asked_for :id
|
78
|
+
user.should_not have_been_asked_for_its :id
|
79
|
+
# user.should_not have_invoked :id
|
80
|
+
user.id.should == 123
|
81
|
+
# user.should have_been_asked_for :id
|
82
|
+
user.should have_been_asked_for_its :id
|
83
|
+
# user.should have_invoked :id
|
84
|
+
# maybe someday also support assertions about order of method invocation
|
85
|
+
|
86
|
+
# set a default
|
87
|
+
user.will_have_name 'Bill'
|
88
|
+
user.name.should == 'Bill'
|
89
|
+
user.should have_been_asked_for_its :name
|
90
|
+
|
91
|
+
# defaults are used if provided
|
92
|
+
Mock::User.new(1).name.should == 'Josh'
|
93
|
+
|
94
|
+
# error is raised if you try to access an attribute that hasn't been set and has no default
|
95
|
+
expect { Mock::User.new(1).address }.to raise_error Surrogate::UnpreparedMethodError
|
96
|
+
Mock::User.new(1).will_have_address('123 Fake St.').address.should == '123 Fake St.'
|
97
|
+
|
98
|
+
# methods with multiple args
|
99
|
+
user.phone_numbers.should be_empty
|
100
|
+
user.add_phone_number '123', '456-7890'
|
101
|
+
user.should have_been_told_to(:add_phone_number).with('123', '456-7890')
|
102
|
+
# user.phone_numbers.should == [['123', '456-7890']] # <-- should we use a hook, or default block to make this happen?
|
103
|
+
|
104
|
+
|
105
|
+
# ===== Substitutability =====
|
106
|
+
|
107
|
+
# real user is not a suitable substitute if missing methods that mock user has
|
108
|
+
user_class.should_not substitute_for Class.new
|
109
|
+
|
110
|
+
# real user must have all of mock user's methods to be substitutable
|
111
|
+
substitutable_real_user_class = Class.new do
|
112
|
+
def self.find() end
|
113
|
+
def initialize(id) end
|
114
|
+
def id() end
|
115
|
+
def name() end
|
116
|
+
def address() end
|
117
|
+
def phone_numbers() end
|
118
|
+
def add_phone_number(area_code, number) end
|
119
|
+
end
|
120
|
+
user_class.should substitute_for substitutable_real_user_class
|
121
|
+
|
122
|
+
# real user class is not a suitable substitutable if has extra methods
|
123
|
+
real_user_class = substitutable_real_user_class.clone
|
124
|
+
def real_user_class.some_class_meth() end
|
125
|
+
user_class.should_not substitute_for real_user_class
|
126
|
+
|
127
|
+
real_user_class = substitutable_real_user_class.clone
|
128
|
+
real_user_class.send(:define_method, :some_instance_method) {}
|
129
|
+
user_class.should_not substitute_for real_user_class
|
130
|
+
end
|
131
|
+
end
|