surrogate 0.5.5 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.rvmrc +0 -3
- data/Changelog.md +21 -0
- data/Readme.md +64 -76
- data/Readme.md.mountain_berry_fields +70 -77
- data/gemfiles/rspec_mocks_2.11 +1 -1
- data/lib/surrogate.rb +1 -1
- data/lib/surrogate/api_comparer.rb +59 -61
- data/lib/surrogate/argument_errorizer.rb +43 -0
- data/lib/surrogate/endower.rb +41 -37
- data/lib/surrogate/hatchery.rb +6 -1
- data/lib/surrogate/hatchling.rb +5 -0
- data/lib/surrogate/method_definition.rb +55 -0
- data/lib/surrogate/porc_reflector.rb +43 -0
- data/lib/surrogate/rspec/invocation_matcher.rb +2 -1
- data/lib/surrogate/rspec/substitute_for.rb +23 -7
- data/lib/surrogate/rspec/with_filter.rb +0 -1
- data/lib/surrogate/surrogate_class_reflector.rb +65 -0
- data/lib/surrogate/surrogate_instance_reflector.rb +15 -0
- data/lib/surrogate/version.rb +1 -1
- data/spec/acceptance_spec.rb +1 -1
- data/spec/defining_api_methods_spec.rb +51 -77
- data/spec/other_shit_spec.rb +131 -0
- data/spec/rspec/block_support_spec.rb +2 -2
- data/spec/rspec/initialization_matcher_spec.rb +12 -4
- data/spec/rspec/rspec_mocks_integration_spec.rb +1 -1
- data/spec/rspec/substitute_for_spec.rb +90 -4
- data/spec/unit/api_comparer_spec.rb +120 -4
- data/spec/unit/argument_errorizer_spec.rb +50 -0
- data/surrogate.gemspec +0 -2
- data/todo +44 -0
- metadata +21 -24
- data/lib/surrogate/options.rb +0 -41
data/.rvmrc
CHANGED
data/Changelog.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
### 0.6.0
|
2
|
+
|
3
|
+
* Setting an override still requires the invoking code to call with the correct signature
|
4
|
+
* Remove `api_method_names` and `api_method_for` and `invocations` from surrogates
|
5
|
+
(might break your code if you relied on these, but they were never advertized, and no obvious reason to use them)
|
6
|
+
Instead use the reflectors: Surrogate::SurrogateClassReflector and Surrogate::SurrogateInstanceReflector
|
7
|
+
* BREAKING CHANGE - Substitutability can check argument "types". This is turned on by default
|
8
|
+
* Initialize is no longer implicitly recorded (This might break something, but I don't think this feature was ever advertized, so hopefully people don't depend on it).
|
9
|
+
* BREAKING CHANGE - API method signatures are enforced (if meth takes 1 arg, you must pass it 1 arg)
|
10
|
+
* The name of a clone is the name of the parent suffixed with '.clone', unless the parent is anonymous (not set to a const), then the name is nil.
|
11
|
+
* Inspect messages are shorter and more helpful
|
12
|
+
* Inspect messages on class clones mimic the parents
|
13
|
+
* Remove comment about the new syntax in the Readme. If you want to switch over, here is a shell script that should get you pretty far:
|
14
|
+
|
15
|
+
find spec -type file |
|
16
|
+
xargs ruby -p -i.old_syntax \
|
17
|
+
-e 'gsub /should(_not)?(\s+)have_been_told_to/, "was\\1\\2told_to"' \
|
18
|
+
-e 'gsub /should(_not)?(\s+)have_been_asked_(if|for)(_its)?/, "was\\1\\2asked_\\3"' \
|
19
|
+
-e 'gsub /should(_not)(\s+)have_been_initialized_with/, "was\\1\\2initialized_with"' \
|
20
|
+
|
21
|
+
|
data/Readme.md
CHANGED
@@ -17,39 +17,17 @@ discouraged at this time. If you do want to do this (e.g. to make an interface f
|
|
17
17
|
let me know, and I'll inform you / fork your gem and help update it, for any breaking changes
|
18
18
|
that I introduce.
|
19
19
|
|
20
|
-
New Syntax
|
21
|
-
==========
|
22
|
-
|
23
|
-
Recently (v0.5.1), a new syntax was added:
|
24
|
-
|
25
|
-
<table>
|
26
|
-
<tr><th>Old</th><th>New</th></tr>
|
27
|
-
<tr><td>.should have_been_told_to</td><td>.was told_to</td></tr>
|
28
|
-
<tr><td>.should have_been_asked_for_its</td><td>.was asked_for</td></tr>
|
29
|
-
<tr><td>.should have_been_asked_if</td><td>.was asked_if</td></tr>
|
30
|
-
<tr><td>.should have_been_initialized_with</td><td>.was initialized_with</td></tr>
|
31
|
-
</table>
|
32
|
-
|
33
|
-
If you want to switch over, here is a shell script that should get you pretty far:
|
34
|
-
|
35
|
-
find spec -type file |
|
36
|
-
xargs ruby -p -i.old_syntax \
|
37
|
-
-e 'gsub /should(_not)?(\s+)have_been_told_to/, "was\\1\\2told_to"' \
|
38
|
-
-e 'gsub /should(_not)?(\s+)have_been_asked_(if|for)(_its)?/, "was\\1\\2asked_\\3"' \
|
39
|
-
-e 'gsub /should(_not)(\s+)have_been_initialized_with/, "was\\1\\2initialized_with"' \
|
40
|
-
|
41
|
-
|
42
20
|
Features
|
43
21
|
========
|
44
22
|
|
45
23
|
* Declarative syntax
|
24
|
+
* Can compare signatures of mocks to signatures of the class being mocked
|
46
25
|
* Support default values
|
47
26
|
* Easily override values
|
48
27
|
* RSpec matchers for asserting what happend (what was invoked, with what args, how many times)
|
49
28
|
* RSpec matchers for asserting the Mock's interface matches the real object
|
50
29
|
* Support for exceptions
|
51
30
|
* Queue return values
|
52
|
-
* Initialization information is always recorded
|
53
31
|
|
54
32
|
|
55
33
|
Usage
|
@@ -86,7 +64,10 @@ end
|
|
86
64
|
MockClient.new.request # => ["result1", "result2"]
|
87
65
|
```
|
88
66
|
|
89
|
-
If you
|
67
|
+
If you expect **arguments**, your block should receive them.
|
68
|
+
This prevents the issues with dynamic mocks where arguments and parameters can diverge.
|
69
|
+
It may seem like more work when you have to write the arguments explicitly, but you only
|
70
|
+
need to do this once, and then you can be sure they match on each invocation.
|
90
71
|
|
91
72
|
```ruby
|
92
73
|
class MockClient
|
@@ -98,6 +79,7 @@ MockClient.new.request 3 # => ["result1", "result2", "result3"]
|
|
98
79
|
```
|
99
80
|
|
100
81
|
You don't need a **default if you set the ivar** of the same name (replace `?` with `_p` for predicates, since you can't have question marks in ivar names)
|
82
|
+
Note that methods without bodies will not have their arguments checked, and will not be asserted against when comparing signatures.
|
101
83
|
|
102
84
|
```ruby
|
103
85
|
class MockClient
|
@@ -177,6 +159,21 @@ user = MockUser.find 12
|
|
177
159
|
user.id # => 12
|
178
160
|
```
|
179
161
|
|
162
|
+
Use **clone** to avoid altering state on the class.
|
163
|
+
```ruby
|
164
|
+
class MockUser
|
165
|
+
Surrogate.endow self do
|
166
|
+
define(:find) { new }
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
user_class = MockUser.clone
|
171
|
+
user_class.find
|
172
|
+
user_class.was told_to :find
|
173
|
+
MockUser.was_not told_to :find
|
174
|
+
```
|
175
|
+
|
176
|
+
|
180
177
|
|
181
178
|
RSpec Integration
|
182
179
|
=================
|
@@ -190,6 +187,21 @@ Load the RSpec matchers.
|
|
190
187
|
require 'surrogate/rspec'
|
191
188
|
```
|
192
189
|
|
190
|
+
Last Instance
|
191
|
+
-------------
|
192
|
+
|
193
|
+
Access the last instance of a class
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
class MockMp3
|
197
|
+
Surrogate.endow self
|
198
|
+
end
|
199
|
+
|
200
|
+
mp3_class = MockMp3.clone # because you don't want to mutate the singleton
|
201
|
+
mp3 = mp3_class.new
|
202
|
+
mp3_class.last_instance.equal? mp3 # => true
|
203
|
+
```
|
204
|
+
|
193
205
|
Nouns
|
194
206
|
-----
|
195
207
|
|
@@ -198,16 +210,16 @@ Given this mock and assuming the following examples happen within a spec
|
|
198
210
|
```ruby
|
199
211
|
class MockMP3
|
200
212
|
Surrogate.endow self
|
201
|
-
define(:info) { 'some info' }
|
213
|
+
define(:info) { |song='Birds Will Sing Forever'| 'some info' }
|
202
214
|
end
|
203
215
|
```
|
204
216
|
|
205
|
-
Check if **was invoked** with `
|
217
|
+
Check if **was invoked** with `was asked_for` (`was` is an alias of `should`, and `asked_for` is a custom matcher)
|
206
218
|
|
207
219
|
```ruby
|
208
|
-
mp3.
|
220
|
+
mp3.was_not asked_for :info
|
209
221
|
mp3.info
|
210
|
-
mp3.
|
222
|
+
mp3.was asked_for :info
|
211
223
|
```
|
212
224
|
|
213
225
|
Invocation **cardinality** by chaining `times(n)`
|
@@ -215,21 +227,21 @@ Invocation **cardinality** by chaining `times(n)`
|
|
215
227
|
```ruby
|
216
228
|
mp3.info
|
217
229
|
mp3.info
|
218
|
-
mp3.
|
230
|
+
mp3.was asked_for(:info).times(2)
|
219
231
|
```
|
220
232
|
|
221
233
|
Invocation **arguments** by chaining `with(args)`
|
222
234
|
|
223
235
|
```ruby
|
224
236
|
mp3.info :title
|
225
|
-
mp3.
|
237
|
+
mp3.was asked_for(:info).with(:title)
|
226
238
|
```
|
227
239
|
|
228
240
|
Supports RSpec's matchers (`no_args`, `hash_including`, etc)
|
229
241
|
|
230
242
|
```ruby
|
231
243
|
mp3.info
|
232
|
-
mp3.
|
244
|
+
mp3.was asked_for(:info).with(no_args)
|
233
245
|
```
|
234
246
|
|
235
247
|
Cardinality of a specific set of args `with(args)` and `times(n)`
|
@@ -238,8 +250,8 @@ Cardinality of a specific set of args `with(args)` and `times(n)`
|
|
238
250
|
mp3.info :title
|
239
251
|
mp3.info :title
|
240
252
|
mp3.info :artist
|
241
|
-
mp3.
|
242
|
-
mp3.
|
253
|
+
mp3.was asked_for(:info).with(:title).times(2)
|
254
|
+
mp3.was asked_for(:info).with(:artist).times(1)
|
243
255
|
```
|
244
256
|
|
245
257
|
|
@@ -255,12 +267,12 @@ class MockMP3
|
|
255
267
|
end
|
256
268
|
```
|
257
269
|
|
258
|
-
Check if **was invoked** with `
|
270
|
+
Check if **was invoked** with `was told_to`
|
259
271
|
|
260
272
|
```ruby
|
261
|
-
mp3.
|
273
|
+
mp3.was_not told_to :play
|
262
274
|
mp3.play
|
263
|
-
mp3.
|
275
|
+
mp3.was told_to :play
|
264
276
|
```
|
265
277
|
|
266
278
|
Also supports the same `with(args)` and `times(n)` that nouns have.
|
@@ -269,7 +281,7 @@ Also supports the same `with(args)` and `times(n)` that nouns have.
|
|
269
281
|
Initialization
|
270
282
|
--------------
|
271
283
|
|
272
|
-
Query with `
|
284
|
+
Query with `was initialized_with`, which is exactly the same as saying `was told_to(:initialize).with(...)`
|
273
285
|
|
274
286
|
```ruby
|
275
287
|
class MockUser
|
@@ -279,14 +291,14 @@ class MockUser
|
|
279
291
|
end
|
280
292
|
user = MockUser.new 12
|
281
293
|
user.id.should == 12
|
282
|
-
user.
|
294
|
+
user.was initialized_with 12
|
283
295
|
```
|
284
296
|
|
285
297
|
|
286
298
|
Predicates
|
287
299
|
----------
|
288
300
|
|
289
|
-
Query qith `
|
301
|
+
Query qith `was asked_if`, all the same chainable methods from above apply.
|
290
302
|
|
291
303
|
```ruby
|
292
304
|
class MockUser
|
@@ -298,15 +310,15 @@ user = MockUser.new
|
|
298
310
|
user.should_not be_admin
|
299
311
|
user.will_have_admin? true
|
300
312
|
user.should be_admin
|
301
|
-
user.
|
313
|
+
user.was asked_if(:admin?).times(2)
|
302
314
|
```
|
303
315
|
|
304
316
|
|
305
|
-
class MockUser
|
306
|
-
|
307
317
|
Substitutability
|
308
318
|
----------------
|
309
319
|
|
320
|
+
Facets of substitutability: method existence, argument types, (and soon argument names)
|
321
|
+
|
310
322
|
After you've implemented the real version of your mock (assuming a [top-down](http://vimeo.com/31267109) style of development),
|
311
323
|
how do you prevent your real object from getting out of synch with your mock?
|
312
324
|
|
@@ -314,8 +326,8 @@ Assert that your mock has the **same interface** as your real class.
|
|
314
326
|
This will fail if the mock inherits methods which are not on the real class. It will also fail
|
315
327
|
if the real class has any methods which have not been defined on the mock or inherited by the mock.
|
316
328
|
|
317
|
-
Presently, it will ignore methods defined directly in the mock (as it
|
318
|
-
|
329
|
+
Presently, it will ignore methods defined directly in the mock (as it generally considers them to be helpers).
|
330
|
+
In a future version, you will be able to tell it to treat other methods
|
319
331
|
as part of the API (will fail if they don't match, and maybe record their values).
|
320
332
|
|
321
333
|
```ruby
|
@@ -348,6 +360,13 @@ class UserWithNameAndAddress < UserWithName
|
|
348
360
|
def address()end
|
349
361
|
end
|
350
362
|
MockUser.should_not substitute_for UserWithNameAndAddress
|
363
|
+
|
364
|
+
# signatures don't match
|
365
|
+
class UserWithWrongSignature
|
366
|
+
def initialize()end # no id
|
367
|
+
def id()end
|
368
|
+
end
|
369
|
+
MockUser.should_not substitute_for UserWithWrongSignature
|
351
370
|
```
|
352
371
|
|
353
372
|
Sometimes you don't want to have to implement the entire interface.
|
@@ -405,7 +424,7 @@ describe 'something that creates a user through the service' do
|
|
405
424
|
to_return
|
406
425
|
end
|
407
426
|
|
408
|
-
service.
|
427
|
+
service.was told_to(:create).with { |block|
|
409
428
|
block.call_with({id: new_id}) # this will be given to the block
|
410
429
|
block.returns old_id # provide a return value, or a block that receives the return value (where you can make assertions)
|
411
430
|
block.before { user_id.should == old_id } # assertions about state of the world before the block is called
|
@@ -442,34 +461,3 @@ Special Thanks
|
|
442
461
|
* [Corey Haines](http://coreyhaines.com/) for pairing on substitutability with me
|
443
462
|
* [Enova](http://www.enovafinancial.com/) for giving me time and motivation to work on this during Enova Labs.
|
444
463
|
* [8th Light](http://8thlight.com/) for giving me time to work on this during our weekly Wazas, and the general encouragement and interest
|
445
|
-
|
446
|
-
|
447
|
-
TODO
|
448
|
-
----
|
449
|
-
|
450
|
-
* Remove dependency on all of RSpec and only depend on rspec-core, then have AC tests for the other shit
|
451
|
-
* Add proper failure messages for block invocations
|
452
|
-
* Add a better explanation for motivations
|
453
|
-
* 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)
|
454
|
-
* don't blow up when delegating to the Object#initialize with args (do I still want this, or do I want to force arity matching (and maybe even variable name matching)?)
|
455
|
-
* config: rspec_mocks loaded, whether unprepared blocks should raise or just return nil
|
456
|
-
* extract surrogate/rspec into its own gem
|
457
|
-
* support subset-substitutabilty not being able to touch real methods (e.g. #respond_to?)
|
458
|
-
* Add a last_instance option so you don't have to track it explicitly
|
459
|
-
* make substitutability matcher go either way
|
460
|
-
* make substitutability matcher not care whether either are surrogates
|
461
|
-
* Add support for operators
|
462
|
-
|
463
|
-
|
464
|
-
Future Features
|
465
|
-
---------------
|
466
|
-
|
467
|
-
* figure out how to talk about callbacks like #on_success
|
468
|
-
* have some sort of reinitialization that can hook into setup/teardown steps of test suite
|
469
|
-
* Support arity checking as part of substitutability
|
470
|
-
* 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)
|
471
|
-
* ability to declare normal methods as being part of the API
|
472
|
-
* ability to declare a define that uses the overridden method as the body, but can still act like an api method
|
473
|
-
* assertions for order of invocations & methods
|
474
|
-
* class generator? (supports a top-down style of development for when you write your mocks before you write your implementations)
|
475
|
-
* deal with hard dependency on rspec-mocks
|
@@ -6,6 +6,7 @@ require 'surrogate'
|
|
6
6
|
<% end %>
|
7
7
|
|
8
8
|
<% context 'generic it block' do %>
|
9
|
+
require 'surrogate/rspec'
|
9
10
|
describe 'whatever' do
|
10
11
|
it 'does something' do
|
11
12
|
__CODE__
|
@@ -28,39 +29,17 @@ discouraged at this time. If you do want to do this (e.g. to make an interface f
|
|
28
29
|
let me know, and I'll inform you / fork your gem and help update it, for any breaking changes
|
29
30
|
that I introduce.
|
30
31
|
|
31
|
-
New Syntax
|
32
|
-
==========
|
33
|
-
|
34
|
-
Recently (v0.5.1), a new syntax was added:
|
35
|
-
|
36
|
-
<table>
|
37
|
-
<tr><th>Old</th><th>New</th></tr>
|
38
|
-
<tr><td>.should have_been_told_to</td><td>.was told_to</td></tr>
|
39
|
-
<tr><td>.should have_been_asked_for_its</td><td>.was asked_for</td></tr>
|
40
|
-
<tr><td>.should have_been_asked_if</td><td>.was asked_if</td></tr>
|
41
|
-
<tr><td>.should have_been_initialized_with</td><td>.was initialized_with</td></tr>
|
42
|
-
</table>
|
43
|
-
|
44
|
-
If you want to switch over, here is a shell script that should get you pretty far:
|
45
|
-
|
46
|
-
find spec -type file |
|
47
|
-
xargs ruby -p -i.old_syntax \
|
48
|
-
-e 'gsub /should(_not)?(\s+)have_been_told_to/, "was\\1\\2told_to"' \
|
49
|
-
-e 'gsub /should(_not)?(\s+)have_been_asked_(if|for)(_its)?/, "was\\1\\2asked_\\3"' \
|
50
|
-
-e 'gsub /should(_not)(\s+)have_been_initialized_with/, "was\\1\\2initialized_with"' \
|
51
|
-
|
52
|
-
|
53
32
|
Features
|
54
33
|
========
|
55
34
|
|
56
35
|
* Declarative syntax
|
36
|
+
* Can compare signatures of mocks to signatures of the class being mocked
|
57
37
|
* Support default values
|
58
38
|
* Easily override values
|
59
39
|
* RSpec matchers for asserting what happend (what was invoked, with what args, how many times)
|
60
40
|
* RSpec matchers for asserting the Mock's interface matches the real object
|
61
41
|
* Support for exceptions
|
62
42
|
* Queue return values
|
63
|
-
* Initialization information is always recorded
|
64
43
|
|
65
44
|
|
66
45
|
Usage
|
@@ -103,7 +82,10 @@ MockClient.new.request # => ["result1", "result2"]
|
|
103
82
|
<% end %>
|
104
83
|
```
|
105
84
|
|
106
|
-
If you
|
85
|
+
If you expect **arguments**, your block should receive them.
|
86
|
+
This prevents the issues with dynamic mocks where arguments and parameters can diverge.
|
87
|
+
It may seem like more work when you have to write the arguments explicitly, but you only
|
88
|
+
need to do this once, and then you can be sure they match on each invocation.
|
107
89
|
|
108
90
|
```ruby
|
109
91
|
<% test 'api method with arguments', with: :magic_comments do %>
|
@@ -117,6 +99,7 @@ MockClient.new.request 3 # => ["result1", "result2", "result3"]
|
|
117
99
|
```
|
118
100
|
|
119
101
|
You don't need a **default if you set the ivar** of the same name (replace `?` with `_p` for predicates, since you can't have question marks in ivar names)
|
102
|
+
Note that methods without bodies will not have their arguments checked, and will not be asserted against when comparing signatures.
|
120
103
|
|
121
104
|
```ruby
|
122
105
|
<% test 'overriding default by setting the ivar', with: :magic_comments do %>
|
@@ -206,6 +189,23 @@ user.id # => 12
|
|
206
189
|
<% end %>
|
207
190
|
```
|
208
191
|
|
192
|
+
Use **clone** to avoid altering state on the class.
|
193
|
+
```ruby
|
194
|
+
<% test 'clone to avoid mutating state', with: :rspec, context: 'generic it block' do %>
|
195
|
+
class MockUser
|
196
|
+
Surrogate.endow self do
|
197
|
+
define(:find) { new }
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
user_class = MockUser.clone
|
202
|
+
user_class.find
|
203
|
+
user_class.was told_to :find
|
204
|
+
MockUser.was_not told_to :find
|
205
|
+
<% end %>
|
206
|
+
```
|
207
|
+
|
208
|
+
|
209
209
|
|
210
210
|
RSpec Integration
|
211
211
|
=================
|
@@ -222,6 +222,23 @@ require 'surrogate/rspec'
|
|
222
222
|
require 'surrogate/rspec'
|
223
223
|
```
|
224
224
|
|
225
|
+
Last Instance
|
226
|
+
-------------
|
227
|
+
|
228
|
+
Access the last instance of a class
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
<% test "last instance", with: :magic_comments do %>
|
232
|
+
class MockMp3
|
233
|
+
Surrogate.endow self
|
234
|
+
end
|
235
|
+
|
236
|
+
mp3_class = MockMp3.clone # because you don't want to mutate the singleton
|
237
|
+
mp3 = mp3_class.new
|
238
|
+
mp3_class.last_instance.equal? mp3 # => true
|
239
|
+
<% end %>
|
240
|
+
```
|
241
|
+
|
225
242
|
Nouns
|
226
243
|
-----
|
227
244
|
|
@@ -231,7 +248,7 @@ Given this mock and assuming the following examples happen within a spec
|
|
231
248
|
<% test "mock mp3 code shouldn't blow up", with: :magic_comments do %>
|
232
249
|
class MockMP3
|
233
250
|
Surrogate.endow self
|
234
|
-
define(:info) { 'some info' }
|
251
|
+
define(:info) { |song='Birds Will Sing Forever'| 'some info' }
|
235
252
|
end
|
236
253
|
<% end %>
|
237
254
|
```
|
@@ -239,7 +256,7 @@ end
|
|
239
256
|
<% context 'mp3 in spec' do %>
|
240
257
|
class MockMP3
|
241
258
|
Surrogate.endow self
|
242
|
-
define(:info) { 'some info' }
|
259
|
+
define(:info) { |song='Birds Will Sing Forever'| 'some info' }
|
243
260
|
end
|
244
261
|
|
245
262
|
describe 'the example' do
|
@@ -250,13 +267,13 @@ describe 'the example' do
|
|
250
267
|
end
|
251
268
|
<% end %>
|
252
269
|
|
253
|
-
Check if **was invoked** with `
|
270
|
+
Check if **was invoked** with `was asked_for` (`was` is an alias of `should`, and `asked_for` is a custom matcher)
|
254
271
|
|
255
272
|
```ruby
|
256
273
|
<% test 'noun invocation', with: :rspec, context: 'mp3 in spec' do %>
|
257
|
-
mp3.
|
274
|
+
mp3.was_not asked_for :info
|
258
275
|
mp3.info
|
259
|
-
mp3.
|
276
|
+
mp3.was asked_for :info
|
260
277
|
<% end %>
|
261
278
|
```
|
262
279
|
|
@@ -266,7 +283,7 @@ Invocation **cardinality** by chaining `times(n)`
|
|
266
283
|
<% test 'noun cardinality', with: :rspec, context: 'mp3 in spec' do %>
|
267
284
|
mp3.info
|
268
285
|
mp3.info
|
269
|
-
mp3.
|
286
|
+
mp3.was asked_for(:info).times(2)
|
270
287
|
<% end %>
|
271
288
|
```
|
272
289
|
|
@@ -275,7 +292,7 @@ Invocation **arguments** by chaining `with(args)`
|
|
275
292
|
```ruby
|
276
293
|
<% test 'noun invocation with args', with: :rspec, context: 'mp3 in spec' do %>
|
277
294
|
mp3.info :title
|
278
|
-
mp3.
|
295
|
+
mp3.was asked_for(:info).with(:title)
|
279
296
|
<% end %>
|
280
297
|
```
|
281
298
|
|
@@ -284,7 +301,7 @@ Supports RSpec's matchers (`no_args`, `hash_including`, etc)
|
|
284
301
|
```ruby
|
285
302
|
<% test 'rspec matchers integration', with: :rspec, context: 'mp3 in spec' do %>
|
286
303
|
mp3.info
|
287
|
-
mp3.
|
304
|
+
mp3.was asked_for(:info).with(no_args)
|
288
305
|
<% end %>
|
289
306
|
```
|
290
307
|
|
@@ -295,8 +312,8 @@ Cardinality of a specific set of args `with(args)` and `times(n)`
|
|
295
312
|
mp3.info :title
|
296
313
|
mp3.info :title
|
297
314
|
mp3.info :artist
|
298
|
-
mp3.
|
299
|
-
mp3.
|
315
|
+
mp3.was asked_for(:info).with(:title).times(2)
|
316
|
+
mp3.was asked_for(:info).with(:artist).times(1)
|
300
317
|
<% end %>
|
301
318
|
```
|
302
319
|
|
@@ -328,13 +345,13 @@ describe 'the example' do
|
|
328
345
|
end
|
329
346
|
<% end %>
|
330
347
|
|
331
|
-
Check if **was invoked** with `
|
348
|
+
Check if **was invoked** with `was told_to`
|
332
349
|
|
333
350
|
```ruby
|
334
351
|
<% test 'have_been_told_to', with: :rspec, context: 'mp3 that plays in spec' do %>
|
335
|
-
mp3.
|
352
|
+
mp3.was_not told_to :play
|
336
353
|
mp3.play
|
337
|
-
mp3.
|
354
|
+
mp3.was told_to :play
|
338
355
|
<% end %>
|
339
356
|
```
|
340
357
|
|
@@ -344,7 +361,7 @@ Also supports the same `with(args)` and `times(n)` that nouns have.
|
|
344
361
|
Initialization
|
345
362
|
--------------
|
346
363
|
|
347
|
-
Query with `
|
364
|
+
Query with `was initialized_with`, which is exactly the same as saying `was told_to(:initialize).with(...)`
|
348
365
|
|
349
366
|
```ruby
|
350
367
|
<% test 'initialization test', with: :rspec, context: 'generic it block' do %>
|
@@ -355,7 +372,7 @@ class MockUser
|
|
355
372
|
end
|
356
373
|
user = MockUser.new 12
|
357
374
|
user.id.should == 12
|
358
|
-
user.
|
375
|
+
user.was initialized_with 12
|
359
376
|
<% end %>
|
360
377
|
```
|
361
378
|
|
@@ -363,7 +380,7 @@ user.should have_been_initialized_with 12
|
|
363
380
|
Predicates
|
364
381
|
----------
|
365
382
|
|
366
|
-
Query qith `
|
383
|
+
Query qith `was asked_if`, all the same chainable methods from above apply.
|
367
384
|
|
368
385
|
```ruby
|
369
386
|
<% test 'initialization test', with: :rspec, context: 'generic it block' do %>
|
@@ -376,16 +393,16 @@ user = MockUser.new
|
|
376
393
|
user.should_not be_admin
|
377
394
|
user.will_have_admin? true
|
378
395
|
user.should be_admin
|
379
|
-
user.
|
396
|
+
user.was asked_if(:admin?).times(2)
|
380
397
|
<% end %>
|
381
398
|
```
|
382
399
|
|
383
400
|
|
384
|
-
class MockUser
|
385
|
-
|
386
401
|
Substitutability
|
387
402
|
----------------
|
388
403
|
|
404
|
+
Facets of substitutability: method existence, argument types, (and soon argument names)
|
405
|
+
|
389
406
|
After you've implemented the real version of your mock (assuming a [top-down](http://vimeo.com/31267109) style of development),
|
390
407
|
how do you prevent your real object from getting out of synch with your mock?
|
391
408
|
|
@@ -393,8 +410,8 @@ Assert that your mock has the **same interface** as your real class.
|
|
393
410
|
This will fail if the mock inherits methods which are not on the real class. It will also fail
|
394
411
|
if the real class has any methods which have not been defined on the mock or inherited by the mock.
|
395
412
|
|
396
|
-
Presently, it will ignore methods defined directly in the mock (as it
|
397
|
-
|
413
|
+
Presently, it will ignore methods defined directly in the mock (as it generally considers them to be helpers).
|
414
|
+
In a future version, you will be able to tell it to treat other methods
|
398
415
|
as part of the API (will fail if they don't match, and maybe record their values).
|
399
416
|
|
400
417
|
```ruby
|
@@ -428,6 +445,13 @@ class UserWithNameAndAddress < UserWithName
|
|
428
445
|
def address()end
|
429
446
|
end
|
430
447
|
MockUser.should_not substitute_for UserWithNameAndAddress
|
448
|
+
|
449
|
+
# signatures don't match
|
450
|
+
class UserWithWrongSignature
|
451
|
+
def initialize()end # no id
|
452
|
+
def id()end
|
453
|
+
end
|
454
|
+
MockUser.should_not substitute_for UserWithWrongSignature
|
431
455
|
<% end %>
|
432
456
|
```
|
433
457
|
|
@@ -489,7 +513,7 @@ describe 'something that creates a user through the service' do
|
|
489
513
|
to_return
|
490
514
|
end
|
491
515
|
|
492
|
-
service.
|
516
|
+
service.was told_to(:create).with { |block|
|
493
517
|
block.call_with({id: new_id}) # this will be given to the block
|
494
518
|
block.returns old_id # provide a return value, or a block that receives the return value (where you can make assertions)
|
495
519
|
block.before { user_id.should == old_id } # assertions about state of the world before the block is called
|
@@ -527,34 +551,3 @@ Special Thanks
|
|
527
551
|
* [Corey Haines](http://coreyhaines.com/) for pairing on substitutability with me
|
528
552
|
* [Enova](http://www.enovafinancial.com/) for giving me time and motivation to work on this during Enova Labs.
|
529
553
|
* [8th Light](http://8thlight.com/) for giving me time to work on this during our weekly Wazas, and the general encouragement and interest
|
530
|
-
|
531
|
-
|
532
|
-
TODO
|
533
|
-
----
|
534
|
-
|
535
|
-
* Remove dependency on all of RSpec and only depend on rspec-core, then have AC tests for the other shit
|
536
|
-
* Add proper failure messages for block invocations
|
537
|
-
* Add a better explanation for motivations
|
538
|
-
* 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)
|
539
|
-
* don't blow up when delegating to the Object#initialize with args (do I still want this, or do I want to force arity matching (and maybe even variable name matching)?)
|
540
|
-
* config: rspec_mocks loaded, whether unprepared blocks should raise or just return nil
|
541
|
-
* extract surrogate/rspec into its own gem
|
542
|
-
* support subset-substitutabilty not being able to touch real methods (e.g. #respond_to?)
|
543
|
-
* Add a last_instance option so you don't have to track it explicitly
|
544
|
-
* make substitutability matcher go either way
|
545
|
-
* make substitutability matcher not care whether either are surrogates
|
546
|
-
* Add support for operators
|
547
|
-
|
548
|
-
|
549
|
-
Future Features
|
550
|
-
---------------
|
551
|
-
|
552
|
-
* figure out how to talk about callbacks like #on_success
|
553
|
-
* have some sort of reinitialization that can hook into setup/teardown steps of test suite
|
554
|
-
* Support arity checking as part of substitutability
|
555
|
-
* 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)
|
556
|
-
* ability to declare normal methods as being part of the API
|
557
|
-
* ability to declare a define that uses the overridden method as the body, but can still act like an api method
|
558
|
-
* assertions for order of invocations & methods
|
559
|
-
* class generator? (supports a top-down style of development for when you write your mocks before you write your implementations)
|
560
|
-
* deal with hard dependency on rspec-mocks
|