surrogate 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|