surrogate 0.3.0 → 0.3.1
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/Readme.md +349 -65
- data/lib/surrogate/endower.rb +7 -7
- data/lib/surrogate/hatchery.rb +1 -5
- data/lib/surrogate/hatchling.rb +20 -38
- data/lib/surrogate/rspec.rb +2 -0
- data/lib/surrogate/values.rb +78 -0
- data/lib/surrogate/version.rb +1 -1
- data/lib/surrogate.rb +3 -4
- data/spec/acceptance_spec.rb +9 -2
- data/spec/spec_helper.rb +0 -1
- metadata +5 -5
- data/lib/surrogate/method_queue.rb +0 -16
data/Readme.md
CHANGED
@@ -1,95 +1,379 @@
|
|
1
|
-
|
1
|
+
About
|
2
|
+
=====
|
3
|
+
|
4
|
+
Handrolling mocks is the best, but involves more overhead than necessary, and usually has less helpful
|
5
|
+
error messages. Surrogate addresses this by endowing your objects with common things that most mocks need.
|
6
|
+
Currently it is only integrated with RSpec.
|
7
|
+
|
8
|
+
|
9
|
+
Features
|
10
|
+
========
|
11
|
+
|
12
|
+
* Declarative syntax
|
13
|
+
* Support default values
|
14
|
+
* Easily override values
|
15
|
+
* RSpec matchers for asserting what happend (what was invoked, with what args, how many times)
|
16
|
+
* RSpec matchers for asserting the Mock's interface matches the real object
|
17
|
+
* Support for exceptions
|
18
|
+
* Queue return values
|
19
|
+
* Initialization information is always recorded
|
20
|
+
|
21
|
+
|
22
|
+
Usage
|
23
|
+
=====
|
24
|
+
|
25
|
+
**Endow** a class with surrogate abilities
|
2
26
|
|
3
27
|
```ruby
|
4
|
-
|
5
|
-
|
28
|
+
class Mock
|
29
|
+
Surrogate.endow self
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
Define a **class method** by using `define` in the block when endowing your class.
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
class MockClient
|
37
|
+
Surrogate.endow self do
|
38
|
+
define(:default_url) { 'http://example.com' }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
MockClient.default_url # => "http://example.com"
|
43
|
+
```
|
44
|
+
|
45
|
+
Define an **instance method** by using `define` outside the block after endowing your class.
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
class MockClient
|
49
|
+
Surrogate.endow self
|
50
|
+
define(:request) { ['result1', 'result2'] }
|
51
|
+
end
|
52
|
+
|
53
|
+
MockClient.new.request # => ["result1", "result2"]
|
54
|
+
```
|
55
|
+
|
56
|
+
If you care about the **arguments**, your block can receive them.
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
class MockClient
|
60
|
+
Surrogate.endow self
|
61
|
+
define(:request) { |limit| limit.times.map { |i| "result#{i.next}" } }
|
62
|
+
end
|
63
|
+
|
64
|
+
MockClient.new.request 3 # => ["result1", "result2", "result3"]
|
65
|
+
```
|
66
|
+
|
67
|
+
You don't need a **default if you set the ivar** of the same name
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
class MockClient
|
71
|
+
Surrogate.endow self
|
72
|
+
define(:initialize) { |id| @id = id }
|
73
|
+
define :id
|
74
|
+
end
|
75
|
+
MockClient.new(12).id # => 12
|
76
|
+
```
|
77
|
+
|
78
|
+
**Override defaults** with `will_<verb>` and `will_have_<noun>`
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
class MockMP3
|
82
|
+
Surrogate.endow self
|
83
|
+
define :play # defaults are optional, will raise error if invoked without being told what to do
|
84
|
+
define :info
|
85
|
+
end
|
86
|
+
|
87
|
+
mp3 = MockMP3.new
|
88
|
+
|
89
|
+
# verbs
|
90
|
+
mp3.will_play true
|
91
|
+
mp3.play # => true
|
92
|
+
|
93
|
+
# nouns
|
94
|
+
mp3.will_have_info artist: 'Symphony of Science', title: 'Children of Africa'
|
95
|
+
mp3.info # => {:artist=>"Symphony of Science", :title=>"Children of Africa"}
|
96
|
+
```
|
97
|
+
|
98
|
+
**Errors** get raised
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
class MockClient
|
102
|
+
Surrogate.endow self
|
103
|
+
define :request
|
104
|
+
end
|
105
|
+
|
106
|
+
client = MockClient.new
|
107
|
+
client.will_have_request StandardError.new('Remote service unavailable')
|
6
108
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
109
|
+
begin
|
110
|
+
client.request
|
111
|
+
rescue StandardError => e
|
112
|
+
e # => #<StandardError: Remote service unavailable>
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
**Queue** up return values
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
class MockPlayer
|
120
|
+
Surrogate.endow self
|
121
|
+
define(:move) { 20 }
|
122
|
+
end
|
123
|
+
|
124
|
+
player = MockPlayer.new
|
125
|
+
player.will_move 1, 9, 3
|
126
|
+
player.move # => 1
|
127
|
+
player.move # => 9
|
128
|
+
player.move # => 3
|
129
|
+
|
130
|
+
# then back to default behaviour (or error if none provided)
|
131
|
+
player.move # => 20
|
132
|
+
```
|
133
|
+
|
134
|
+
You can define **initialize**
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
class MockUser
|
138
|
+
Surrogate.endow self do
|
139
|
+
define(:find) { |id| new id }
|
15
140
|
end
|
141
|
+
define(:initialize) { |id| @id = id }
|
142
|
+
define(:id) { @id }
|
143
|
+
end
|
144
|
+
|
145
|
+
user = MockUser.find 12
|
146
|
+
user.id # => 12
|
147
|
+
```
|
148
|
+
|
149
|
+
|
150
|
+
RSpec Integration
|
151
|
+
=================
|
152
|
+
|
153
|
+
Currently only integrated with RSpec, since that's what I use. It has some builtin matchers
|
154
|
+
for querying what happened.
|
155
|
+
|
156
|
+
Load the RSpec matchers.
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
require 'surrogate/rspec'
|
160
|
+
```
|
161
|
+
|
162
|
+
Nouns
|
163
|
+
-----
|
164
|
+
|
165
|
+
Given this mock and assuming the following examples happen within a spec
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
class MockMP3
|
169
|
+
Surrogate.endow self
|
170
|
+
define(:info) { 'some info' }
|
16
171
|
end
|
172
|
+
```
|
173
|
+
|
174
|
+
Check if **was invoked** with `have_been_asked_for_its`
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
mp3.should_not have_been_asked_for_its :info
|
178
|
+
mp3.info
|
179
|
+
mp3.should have_been_asked_for_its :info
|
180
|
+
```
|
181
|
+
|
182
|
+
Invocation **cardinality** by chaining `times(n)`
|
183
|
+
|
184
|
+
```ruby
|
185
|
+
mp3.info
|
186
|
+
mp3.info
|
187
|
+
mp3.should have_been_asked_for_its(:info).times(2)
|
188
|
+
```
|
189
|
+
|
190
|
+
Invocation **arguments** by chaining `with(args)`
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
mp3.info :title
|
194
|
+
mp3.should have_been_asked_for_its(:info).with(:title)
|
195
|
+
```
|
196
|
+
|
197
|
+
Supports RSpec's `no_args` matcher (the others coming in future versions)
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
mp3.info
|
201
|
+
mp3.should have_been_asked_for_its(:info).with(no_args)
|
202
|
+
```
|
203
|
+
|
204
|
+
Cardinality of a specific set of args `with(args)` and `times(n)`
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
mp3.info :title
|
208
|
+
mp3.info :title
|
209
|
+
mp3.info :artist
|
210
|
+
mp3.should have_been_asked_for_its(:info).with(:title).times(2)
|
211
|
+
mp3.should have_been_asked_for_its(:info).with(:artist).times(1)
|
212
|
+
```
|
213
|
+
|
214
|
+
|
215
|
+
Verbs
|
216
|
+
-----
|
217
|
+
|
218
|
+
Given this mock and assuming the following examples happen within a spec
|
219
|
+
|
220
|
+
```ruby
|
221
|
+
class MockMP3
|
222
|
+
Surrogate.endow self
|
223
|
+
define(:play) { true }
|
224
|
+
end
|
225
|
+
```
|
226
|
+
|
227
|
+
Check if **was invoked** with `have_been_told_to`
|
228
|
+
|
229
|
+
```ruby
|
230
|
+
mp3.should_not have_been_told_to :play
|
231
|
+
mp3.play
|
232
|
+
mp3.should have_been_told_to :play
|
233
|
+
```
|
234
|
+
|
235
|
+
Also supports the same `with(args)` and `times(n)` that nouns have.
|
236
|
+
|
237
|
+
|
238
|
+
Initialization
|
239
|
+
--------------
|
240
|
+
|
241
|
+
Query with `have_been_initialized_with`, which is exactly the same as saying `have_been_told_to(:initialize).with(...)`
|
242
|
+
|
243
|
+
```ruby
|
244
|
+
class MockUser
|
245
|
+
Surrogate.endow self
|
246
|
+
define(:initialize) { |id| @id = id }
|
247
|
+
define :id
|
248
|
+
end
|
249
|
+
user = MockUser.new 12
|
250
|
+
user.id.should == 12
|
251
|
+
user.should have_been_initialized_with 12
|
252
|
+
```
|
253
|
+
|
254
|
+
Initialization is **always recorded**, so that you don't have to override it just to be able to query.
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
class MockUser < Struct.new(:id)
|
258
|
+
Surrogate.endow self
|
259
|
+
end
|
260
|
+
user = MockUser.new 12
|
261
|
+
user.id.should == 12
|
262
|
+
user.should have_been_initialized_with 12
|
263
|
+
```
|
264
|
+
|
265
|
+
|
266
|
+
Substitutability
|
267
|
+
----------------
|
17
268
|
|
269
|
+
After you've implemented the real version of your mock (assuming a [top-down](http://vimeo.com/31267109) style of development),
|
270
|
+
how do you prevent your real object from getting out of synch with your mock?
|
271
|
+
|
272
|
+
Assert that your mock has the **same interface** as your real class.
|
273
|
+
This will fail if the mock inherits methods methods not on the real class. And it will fail
|
274
|
+
if the real class has or lacks any methods defined on the mock or inherited by the mock.
|
275
|
+
|
276
|
+
Presently, it will ignore methods defined directly in the mock (as it adds quite a few of its own methods,
|
277
|
+
and generally considers them to be helpers). In a future version, you will be able to tell it to treat other methods
|
278
|
+
as part of the API (will fail if they don't match, and maybe record their values).
|
279
|
+
|
280
|
+
```ruby
|
18
281
|
class User
|
282
|
+
def initialize(id)end
|
283
|
+
def id()end
|
284
|
+
end
|
285
|
+
|
286
|
+
class MockUser
|
287
|
+
Surrogate.endow self
|
288
|
+
define(:initialize) { |id| @id = id }
|
289
|
+
define :id
|
290
|
+
end
|
291
|
+
|
292
|
+
# they are the same
|
293
|
+
MockUser.should substitute_for User
|
294
|
+
|
295
|
+
# mock has extra method
|
296
|
+
MockUser.define :name
|
297
|
+
MockUser.should_not substitute_for User
|
298
|
+
|
299
|
+
# the same again via inheritance
|
300
|
+
class UserWithName < User
|
19
301
|
def name()end
|
20
|
-
def phone_numbers()end
|
21
|
-
def add_phone_number()end
|
22
302
|
end
|
303
|
+
MockUser.should substitute_for UserWithName
|
23
304
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
305
|
+
# real class has extra methods
|
306
|
+
class UserWithNameAndAddress < UserWithName
|
307
|
+
def address()end
|
308
|
+
end
|
309
|
+
MockUser.should_not substitute_for UserWithNameAndAddress
|
310
|
+
```
|
28
311
|
|
29
|
-
|
312
|
+
Sometimes you don't want to have to implement the entire interface.
|
313
|
+
In these cases, you can assert that the methods on the mock are a **subset**
|
314
|
+
of the methods on the real class.
|
30
315
|
|
31
|
-
|
32
|
-
|
316
|
+
```ruby
|
317
|
+
class User
|
318
|
+
def initialize(id)end
|
319
|
+
def id()end
|
320
|
+
def name()end
|
321
|
+
end
|
33
322
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
323
|
+
class MockUser
|
324
|
+
Surrogate.endow self
|
325
|
+
define(:initialize) { |id| @id = id }
|
326
|
+
define :id
|
38
327
|
end
|
328
|
+
|
329
|
+
# doesn't matter that real user has a name as long as it has initialize and id
|
330
|
+
MockUser.should substitute_for User, subset: true
|
331
|
+
|
332
|
+
# but now it fails b/c it has no addres
|
333
|
+
MockUser.define :address
|
334
|
+
MockUser.should_not substitute_for User, subset: true
|
39
335
|
```
|
40
336
|
|
41
337
|
|
42
|
-
|
43
|
-
|
338
|
+
How do I introduce my mocks?
|
339
|
+
============================
|
44
340
|
|
45
|
-
|
46
|
-
|
341
|
+
This is known as dependency injection. There are many ways you can do this, you can pass the object into
|
342
|
+
the initializer, you can pass a factory to your class, you can give the class that depends on the mock a
|
343
|
+
setter and then override it whenever you feel it is necessary, you can use RSpec's `#stub` method to put
|
344
|
+
it into place.
|
47
345
|
|
346
|
+
Personally, I use [Deject](https://rubygems.org/gems/deject) another gem I wrote. For more on why I feel
|
347
|
+
it is a better solution than the above methods, see it's [readme](https://github.com/JoshCheek/deject/tree/938edc985c65358c074a7c7b7bbf18dc11e9450e#why-write-this).
|
48
348
|
|
49
|
-
Features for future vuersions
|
50
|
-
-----------------------------
|
51
349
|
|
52
|
-
|
53
|
-
|
54
|
-
* support for raising errors
|
55
|
-
* need some way to talk about and record blocks being passed
|
56
|
-
* support all rspec matchers (RSpec::Mocks::ArgumentMatchers)
|
57
|
-
* assertions for order of invocations & methods
|
350
|
+
But why write this?
|
351
|
+
===================
|
58
352
|
|
353
|
+
Need to put an explanation here soon. In the meantime, I wrote a [blog](http://blog.8thlight.com/josh-cheek/2011/11/28/three-reasons-to-roll-your-own-mocks.html) that touches on the reasons.
|
59
354
|
|
60
355
|
|
61
|
-
|
356
|
+
Special Thanks
|
357
|
+
==============
|
62
358
|
|
63
|
-
|
359
|
+
* [Corey Haines](http://coreyhaines.com/) for pairing on it with me
|
360
|
+
* [8th Light](http://8thlight.com/) for giving me time to work on this during our weekly Wazas, and the general encouragement and interest
|
64
361
|
|
65
|
-
# real user is not a suitable substitute if missing methods that mock user has
|
66
|
-
user_class.should_not substitute_for Class.new
|
67
362
|
|
68
|
-
|
69
|
-
|
70
|
-
def self.find() end
|
71
|
-
def initialize(id) end
|
72
|
-
def id() end
|
73
|
-
def name() end
|
74
|
-
def address() end
|
75
|
-
def phone_numbers() end
|
76
|
-
def add_phone_number(area_code, number) end
|
77
|
-
end
|
78
|
-
user_class.should substitute_for substitutable_real_user_class
|
79
|
-
user_class.should be_subset_of substitutable_real_user_class
|
363
|
+
TODO
|
364
|
+
----
|
80
365
|
|
81
|
-
|
82
|
-
|
83
|
-
def real_user_class.some_class_meth() end
|
84
|
-
user_class.should_not substitute_for real_user_class
|
85
|
-
user_class.should be_subset_of real_user_class
|
366
|
+
* Add a better explanation for motivations
|
367
|
+
* Figure out whether I'm supposed to be using clone or dup for the object -.^ (looks like there may also be an `initialize_copy` method I can take advantage of instead of crazy stupid shit I'm doing now)
|
86
368
|
|
87
|
-
real_user_class = substitutable_real_user_class.clone
|
88
|
-
real_user_class.send(:define_method, :some_instance_method) {}
|
89
|
-
user_class.should_not substitute_for real_user_class
|
90
|
-
user_class.should be_subset_of real_user_class
|
91
369
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
370
|
+
Future Features
|
371
|
+
---------------
|
372
|
+
|
373
|
+
* Support all RSpec matchers (hash_including, anything, etc. see them in RSpec::Mocks::ArgumentMatchers)
|
374
|
+
* have some sort of reinitialization that can hook into setup/teardown steps of test suite
|
375
|
+
* Support arity checking as part of substitutability
|
376
|
+
* Support for blocks
|
377
|
+
* Ability to disassociate the method name from the test (e.g. you shouldn't need to change a test just because you change a name)
|
378
|
+
* declare normal methods as being part of the API (e.g. for inheritance)
|
379
|
+
* assertions for order of invocations & methods
|
data/lib/surrogate/endower.rb
CHANGED
@@ -2,16 +2,16 @@ class Surrogate
|
|
2
2
|
|
3
3
|
# Adds surrogate behaviour to your class / singleton class / instances
|
4
4
|
#
|
5
|
-
# please refactor me!
|
5
|
+
# please refactor me! ...may not be possible :(
|
6
6
|
class Endower
|
7
|
-
def self.endow(klass, &
|
8
|
-
new(klass, &
|
7
|
+
def self.endow(klass, &block)
|
8
|
+
new(klass, &block).endow
|
9
9
|
end
|
10
10
|
|
11
|
-
attr_accessor :klass, :
|
11
|
+
attr_accessor :klass, :block
|
12
12
|
|
13
|
-
def initialize(klass, &
|
14
|
-
self.klass, self.
|
13
|
+
def initialize(klass, &block)
|
14
|
+
self.klass, self.block = klass, block
|
15
15
|
end
|
16
16
|
|
17
17
|
def endow
|
@@ -33,7 +33,7 @@ class Surrogate
|
|
33
33
|
def endow_singleton_class
|
34
34
|
hatchery = add_hatchery_to singleton
|
35
35
|
enable_defining_methods singleton
|
36
|
-
singleton.module_eval &
|
36
|
+
singleton.module_eval &block if block
|
37
37
|
klass.instance_variable_set :@hatchling, Hatchling.new(klass, hatchery)
|
38
38
|
klass
|
39
39
|
end
|
data/lib/surrogate/hatchery.rb
CHANGED
@@ -49,11 +49,7 @@ class Surrogate
|
|
49
49
|
|
50
50
|
def add_helpers_for(method_name, helper_name)
|
51
51
|
klass.send :define_method, helper_name do |*args, &block|
|
52
|
-
|
53
|
-
@hatchling.prepare_method method_name, args, &block
|
54
|
-
else
|
55
|
-
@hatchling.prepare_method_queue method_name, args, &block
|
56
|
-
end
|
52
|
+
@hatchling.prepare_method method_name, args, &block
|
57
53
|
self
|
58
54
|
end
|
59
55
|
end
|
data/lib/surrogate/hatchling.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
class Surrogate
|
2
2
|
UnknownMethod = Class.new StandardError
|
3
3
|
|
4
|
+
|
4
5
|
# This contains the unique behaviour for each instance
|
5
6
|
# It handles method invocation and records the appropriate information
|
6
7
|
class Hatchling
|
@@ -17,31 +18,34 @@ class Surrogate
|
|
17
18
|
def invoke_method(method_name, args, &block)
|
18
19
|
invoked_methods[method_name] << args
|
19
20
|
return get_default method_name, args unless has_ivar? method_name
|
20
|
-
|
21
|
-
|
22
|
-
# This may soon need classes for each type which know how to invoke themselves
|
23
|
-
case ivar
|
24
|
-
when MethodQueue
|
25
|
-
play_from_queue ivar, method_name
|
26
|
-
when Exception
|
27
|
-
raise ivar
|
28
|
-
else
|
29
|
-
ivar
|
30
|
-
end
|
21
|
+
Value.factory(get_ivar method_name).value(self, method_name)
|
31
22
|
end
|
32
23
|
|
33
24
|
def prepare_method(method_name, args, &block)
|
34
|
-
set_ivar method_name, *args
|
35
|
-
end
|
36
|
-
|
37
|
-
def prepare_method_queue(method_name, args, &block)
|
38
|
-
set_ivar method_name, MethodQueue.new(args)
|
25
|
+
set_ivar method_name, Value.factory(*args, &block)
|
39
26
|
end
|
40
27
|
|
41
28
|
def invocations(method_name)
|
42
29
|
invoked_methods[method_name]
|
43
30
|
end
|
44
31
|
|
32
|
+
# maybe these four should be extracted into their own class
|
33
|
+
def has_ivar?(method_name)
|
34
|
+
instance.instance_variable_defined? "@#{method_name}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def set_ivar(method_name, value)
|
38
|
+
instance.instance_variable_set "@#{method_name}", value
|
39
|
+
end
|
40
|
+
|
41
|
+
def get_ivar(method_name)
|
42
|
+
instance.instance_variable_get "@#{method_name}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def unset_ivar(method_name)
|
46
|
+
instance.send :remove_instance_variable, "@#{method_name}"
|
47
|
+
end
|
48
|
+
|
45
49
|
private
|
46
50
|
|
47
51
|
def invoked_methods
|
@@ -57,33 +61,11 @@ class Surrogate
|
|
57
61
|
end
|
58
62
|
end
|
59
63
|
|
60
|
-
def play_from_queue(queue, method_name)
|
61
|
-
result = queue.dequeue
|
62
|
-
unset_ivar method_name if queue.empty?
|
63
|
-
result
|
64
|
-
end
|
65
|
-
|
66
64
|
def must_know(method_name)
|
67
65
|
return if api_methods.has_key? method_name
|
68
66
|
known_methods = api_methods.keys.map(&:to_s).map(&:inspect).join ', '
|
69
67
|
raise UnknownMethod, "doesn't know \"#{method_name}\", only knows #{known_methods}"
|
70
68
|
end
|
71
|
-
|
72
|
-
def has_ivar?(method_name)
|
73
|
-
instance.instance_variable_defined? "@#{method_name}"
|
74
|
-
end
|
75
|
-
|
76
|
-
def set_ivar(method_name, value)
|
77
|
-
instance.instance_variable_set "@#{method_name}", value
|
78
|
-
end
|
79
|
-
|
80
|
-
def get_ivar(method_name)
|
81
|
-
instance.instance_variable_get "@#{method_name}"
|
82
|
-
end
|
83
|
-
|
84
|
-
def unset_ivar(method_name)
|
85
|
-
instance.send :remove_instance_variable, "@#{method_name}"
|
86
|
-
end
|
87
69
|
end
|
88
70
|
end
|
89
71
|
|
data/lib/surrogate/rspec.rb
CHANGED
@@ -0,0 +1,78 @@
|
|
1
|
+
class Surrogate
|
2
|
+
|
3
|
+
# Superclass for all types of values. Where a value is anything stored
|
4
|
+
# in an instance variable on a surrogate, intended to be returned by an api method
|
5
|
+
class Value
|
6
|
+
|
7
|
+
# convert raw arguments into a value
|
8
|
+
def self.factory(*args, &block)
|
9
|
+
arg = args.first
|
10
|
+
if args.size > 1
|
11
|
+
MethodQueue.new args
|
12
|
+
elsif arg.kind_of? Exception
|
13
|
+
Raisable.new arg
|
14
|
+
elsif arg.kind_of? Value
|
15
|
+
Recursive.new arg
|
16
|
+
else
|
17
|
+
Value.new arg
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(value)
|
22
|
+
@value = value
|
23
|
+
end
|
24
|
+
|
25
|
+
def value(hatchling, method_name)
|
26
|
+
@value
|
27
|
+
end
|
28
|
+
|
29
|
+
def factory(*args, &block)
|
30
|
+
self.class.factory(*args, &block)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
# the current set of possible values
|
37
|
+
|
38
|
+
class Surrogate
|
39
|
+
class Value
|
40
|
+
|
41
|
+
class Raisable < Value
|
42
|
+
def value(*)
|
43
|
+
raise @value
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
class Recursive < Value
|
49
|
+
def value(hatchling, method_name)
|
50
|
+
@value.value hatchling, method_name
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
class MethodQueue < Value
|
56
|
+
QueueEmpty = Class.new StandardError
|
57
|
+
|
58
|
+
def value(hatchling, method_name)
|
59
|
+
factory(dequeue).value(hatchling, method_name)
|
60
|
+
ensure
|
61
|
+
hatchling.unset_ivar method_name if empty?
|
62
|
+
end
|
63
|
+
|
64
|
+
def queue
|
65
|
+
@value
|
66
|
+
end
|
67
|
+
|
68
|
+
def dequeue
|
69
|
+
raise QueueEmpty if empty?
|
70
|
+
queue.shift
|
71
|
+
end
|
72
|
+
|
73
|
+
def empty?
|
74
|
+
queue.empty?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/surrogate/version.rb
CHANGED
data/lib/surrogate.rb
CHANGED
@@ -2,16 +2,15 @@ require 'surrogate/version'
|
|
2
2
|
require 'surrogate/hatchling'
|
3
3
|
require 'surrogate/hatchery'
|
4
4
|
require 'surrogate/options'
|
5
|
-
require 'surrogate/
|
5
|
+
require 'surrogate/values'
|
6
6
|
require 'surrogate/endower'
|
7
7
|
require 'surrogate/api_comparer'
|
8
8
|
|
9
9
|
class Surrogate
|
10
10
|
UnpreparedMethodError = Class.new StandardError
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
Endower.endow klass, &playlist
|
12
|
+
def self.endow(klass, &block)
|
13
|
+
Endower.endow klass, &block
|
15
14
|
klass
|
16
15
|
end
|
17
16
|
end
|
data/spec/acceptance_spec.rb
CHANGED
@@ -125,14 +125,21 @@ describe Surrogate do
|
|
125
125
|
def add_phone_number(area_code, number) end
|
126
126
|
end
|
127
127
|
user_class.should substitute_for substitutable_real_user_class
|
128
|
+
user_class.should substitute_for substitutable_real_user_class, subset: true
|
128
129
|
|
129
|
-
# real user class
|
130
|
+
# when real user class has extra methods, it is only substitutable as a subset
|
130
131
|
real_user_class = substitutable_real_user_class.clone
|
131
132
|
def real_user_class.some_class_meth() end
|
132
133
|
user_class.should_not substitute_for real_user_class
|
133
134
|
|
134
|
-
real_user_class = substitutable_real_user_class.
|
135
|
+
real_user_class = substitutable_real_user_class.dup
|
135
136
|
real_user_class.send(:define_method, :some_instance_method) {}
|
136
137
|
user_class.should_not substitute_for real_user_class
|
138
|
+
user_class.should substitute_for real_user_class, subset: true
|
139
|
+
|
140
|
+
# subset substitutability does not work for superset
|
141
|
+
real_user_class = substitutable_real_user_class.dup
|
142
|
+
real_user_class.send :undef_method, :address
|
143
|
+
user_class.should_not substitute_for real_user_class, subset: true
|
137
144
|
end
|
138
145
|
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: surrogate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-05-01 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
16
|
-
requirement: &
|
16
|
+
requirement: &70186184231320 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,7 +21,7 @@ dependencies:
|
|
21
21
|
version: 2.8.0
|
22
22
|
type: :development
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70186184231320
|
25
25
|
description: Framework to aid in handrolling mock/spy objects.
|
26
26
|
email:
|
27
27
|
- josh.cheek@gmail.com
|
@@ -39,11 +39,11 @@ files:
|
|
39
39
|
- lib/surrogate/endower.rb
|
40
40
|
- lib/surrogate/hatchery.rb
|
41
41
|
- lib/surrogate/hatchling.rb
|
42
|
-
- lib/surrogate/method_queue.rb
|
43
42
|
- lib/surrogate/options.rb
|
44
43
|
- lib/surrogate/rspec.rb
|
45
44
|
- lib/surrogate/rspec/api_method_matchers.rb
|
46
45
|
- lib/surrogate/rspec/substitutability_matchers.rb
|
46
|
+
- lib/surrogate/values.rb
|
47
47
|
- lib/surrogate/version.rb
|
48
48
|
- spec/acceptance_spec.rb
|
49
49
|
- spec/defining_api_methods_spec.rb
|
@@ -1,16 +0,0 @@
|
|
1
|
-
class Surrogate
|
2
|
-
class MethodQueue < Struct.new(:queue)
|
3
|
-
QueueEmpty = Class.new StandardError
|
4
|
-
|
5
|
-
def dequeue
|
6
|
-
raise QueueEmpty if empty?
|
7
|
-
current = queue.shift
|
8
|
-
raise current if current.kind_of? Exception
|
9
|
-
current
|
10
|
-
end
|
11
|
-
|
12
|
-
def empty?
|
13
|
-
queue.empty?
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|