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.
@@ -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
+
@@ -0,0 +1,2 @@
1
+ require 'surrogate/rspec/api_method_matchers'
2
+ require 'surrogate/rspec/substitutability_matchers'
@@ -0,0 +1,3 @@
1
+ class Surrogate
2
+ VERSION = "0.1.0"
3
+ end
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