surrogate 0.4.3 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/Readme.md CHANGED
@@ -127,9 +127,6 @@ player.will_move 1, 9, 3
127
127
  player.move # => 1
128
128
  player.move # => 9
129
129
  player.move # => 3
130
-
131
- # then back to default behaviour (or error if none provided)
132
- player.move # => 20
133
130
  ```
134
131
 
135
132
  You can define **initialize**
@@ -260,8 +257,8 @@ After you've implemented the real version of your mock (assuming a [top-down](ht
260
257
  how do you prevent your real object from getting out of synch with your mock?
261
258
 
262
259
  Assert that your mock has the **same interface** as your real class.
263
- This will fail if the mock inherits methods methods not on the real class. And it will fail
264
- if the real class has or lacks any methods defined on the mock or inherited by the mock.
260
+ This will fail if the mock inherits methods which are not on the real class. It will also fail
261
+ if the real class has any methods which have not been defined on the mock or inherited by the mock.
265
262
 
266
263
  Presently, it will ignore methods defined directly in the mock (as it adds quite a few of its own methods,
267
264
  and generally considers them to be helpers). In a future version, you will be able to tell it to treat other methods
@@ -325,6 +322,46 @@ MockUser.should_not substitute_for User, subset: true
325
322
  ```
326
323
 
327
324
 
325
+ Blocks
326
+ ------
327
+
328
+ When your method is invoked with a block, you can make assertions about the block.
329
+
330
+ _Note: Right now, block error messages have not been addressed (which means they are probably confusing as shit)_
331
+
332
+ Before/after hooks (make assertions here)
333
+
334
+ ```ruby
335
+ class MockService
336
+ Surrogate.endow self
337
+ define(:create) {}
338
+ end
339
+
340
+ describe 'something that creates a user through the service' do
341
+ let(:old_id) { 12 }
342
+ let(:new_id) { 123 }
343
+
344
+ it 'updates the user_id and returns the old_id' do
345
+ user_id = old_id
346
+ service = MockService.new
347
+
348
+ service.create do |user|
349
+ to_return = user_id
350
+ user_id = user[:id]
351
+ to_return
352
+ end
353
+
354
+ service.should have_been_told_to(:create).with { |block|
355
+ block.call_with({id: new_id}) # this will be given to the block
356
+ block.returns old_id # provide a return value, or a block that receives the return value (where you can make assertions)
357
+ block.before { user_id.should == old_id } # assertions about state of the world before the block is called
358
+ block.after { user_id.should == new_id } # assertions about the state of the world after the block is called
359
+ }
360
+ end
361
+ end
362
+ ```
363
+
364
+
328
365
  How do I introduce my mocks?
329
366
  ============================
330
367
 
@@ -356,22 +393,27 @@ Special Thanks
356
393
  TODO
357
394
  ----
358
395
 
396
+ * Add proper failure messages for block invocations
397
+ * Add `was told_to` syntax
398
+ * Add support for predicates
359
399
  * Add a better explanation for motivations
360
400
  * 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)
361
401
  * don't blow up when delegating to the Object#initialize with args
362
- * queues should not reset when done, they should raise
402
+ * config: rspec_mocks loaded, whether unprepared blocks should raise or just return nil
403
+ * extract surrogate/rspec into its own gem
404
+ * support subset-substitutabilty not being able to touch real methods (e.g. #respond_to?)
405
+ * Add a last_instance option so you don't have to track it explicitly
363
406
 
364
407
 
365
408
  Future Features
366
409
  ---------------
367
410
 
411
+ * figure out how to talk about callbacks like #on_success
368
412
  * have some sort of reinitialization that can hook into setup/teardown steps of test suite
369
413
  * Support arity checking as part of substitutability
370
- * Support for blocks
371
414
  * 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)
372
- * declare normal methods as being part of the API (e.g. for inheritance)
415
+ * ability to declare normal methods as being part of the API
416
+ * ability to declare a define that uses the overridden method as the body, but can still act like an api method
373
417
  * assertions for order of invocations & methods
374
418
  * class generator? (supports a top-down style of development for when you write your mocks before you write your implementations)
375
- * extract surrogate/rspec into its own gem?
376
419
  * deal with hard dependency on rspec-mocks
377
- * support subset-substitutabilty not being able to touch real methods (e.g. #respond_to?)
@@ -1,5 +1,6 @@
1
1
  class Surrogate
2
- UnknownMethod = Class.new StandardError
2
+ SurrogateError = Class.new StandardError
3
+ UnknownMethod = Class.new SurrogateError
3
4
 
4
5
 
5
6
  # This contains the unique behaviour for each instance
@@ -16,9 +17,10 @@ class Surrogate
16
17
  end
17
18
 
18
19
  def invoke_method(method_name, args, &block)
19
- invoked_methods[method_name] << args
20
- return get_default method_name, args, &block unless has_ivar? method_name
21
- Value.factory(get_ivar method_name).value(self, method_name)
20
+ invocation = Invocation.new(args, &block)
21
+ invoked_methods[method_name] << invocation
22
+ return get_default method_name, invocation, &block unless has_ivar? method_name
23
+ Value.factory(get_ivar method_name).value(method_name)
22
24
  end
23
25
 
24
26
  def prepare_method(method_name, args, &block)
@@ -29,7 +31,28 @@ class Surrogate
29
31
  invoked_methods[method_name]
30
32
  end
31
33
 
32
- # maybe these four should be extracted into their own class
34
+ private
35
+
36
+ def invoked_methods
37
+ @invoked_methods ||= Hash.new do |hash, method_name|
38
+ must_know method_name
39
+ hash[method_name] = []
40
+ end
41
+ end
42
+
43
+ def get_default(method_name, invocation)
44
+ api_methods[method_name].default instance, invocation do
45
+ raise UnpreparedMethodError, "#{method_name} has been invoked without being told how to behave"
46
+ end
47
+ end
48
+
49
+ def must_know(method_name)
50
+ return if api_methods.has_key? method_name
51
+ known_methods = api_methods.keys.map(&:to_s).map(&:inspect).join ', '
52
+ raise UnknownMethod, "doesn't know \"#{method_name}\", only knows #{known_methods}"
53
+ end
54
+
55
+ # maybe these ivar methods should be extracted into their own class
33
56
  def has_ivar?(method_name)
34
57
  instance.instance_variable_defined? ivar_for method_name
35
58
  end
@@ -55,26 +78,6 @@ class Surrogate
55
78
  end
56
79
  end
57
80
 
58
- private
59
-
60
- def invoked_methods
61
- @invoked_methods ||= Hash.new do |hash, method_name|
62
- must_know method_name
63
- hash[method_name] = []
64
- end
65
- end
66
-
67
- def get_default(method_name, args, &block)
68
- api_methods[method_name].default instance, args, block do
69
- raise UnpreparedMethodError, "#{method_name} has been invoked without being told how to behave"
70
- end
71
- end
72
-
73
- def must_know(method_name)
74
- return if api_methods.has_key? method_name
75
- known_methods = api_methods.keys.map(&:to_s).map(&:inspect).join ', '
76
- raise UnknownMethod, "doesn't know \"#{method_name}\", only knows #{known_methods}"
77
- end
78
81
  end
79
82
  end
80
83
 
@@ -0,0 +1,17 @@
1
+ class Surrogate
2
+ class Invocation
3
+ attr_accessor :args, :block
4
+
5
+ def initialize(args, &block)
6
+ self.args, self.block = args, block
7
+ end
8
+
9
+ def has_block?
10
+ !!block
11
+ end
12
+
13
+ def ==(invocation)
14
+ args == invocation.args && has_block? == invocation.has_block?
15
+ end
16
+ end
17
+ end
@@ -20,7 +20,7 @@ class Surrogate
20
20
  options
21
21
  end
22
22
 
23
- def default(instance, args, block, &no_default)
23
+ def default(instance, invocation, &no_default)
24
24
  if options.has_key? :default
25
25
  options[:default]
26
26
  elsif default_proc
@@ -31,7 +31,7 @@ class Surrogate
31
31
  # the options, then we only have to bind it to an instance and invoke
32
32
  BindableBlock.new(instance.class, &default_proc)
33
33
  .bind(instance)
34
- .call(*args, &block)
34
+ .call(*invocation.args, &invocation.block)
35
35
  else
36
36
  no_default.call
37
37
  end
@@ -0,0 +1,59 @@
1
+ class Surrogate
2
+ module RSpec
3
+ class AbstractFailureMessage
4
+ class ArgsInspector
5
+ def self.inspect(invocation)
6
+ inspected_arguments = invocation.args.map { |argument| inspect_argument argument }
7
+ inspected_arguments << 'no args' if inspected_arguments.empty?
8
+ "`" << inspected_arguments.join(", ") << "'"
9
+ end
10
+
11
+ def self.inspect_argument(to_inspect)
12
+ if RSpec.rspec_mocks_loaded? && to_inspect.respond_to?(:description)
13
+ to_inspect.description
14
+ else
15
+ to_inspect.inspect
16
+ end
17
+ end
18
+ end
19
+
20
+ attr_accessor :method_name, :invocations, :with_filter, :times_predicate
21
+
22
+ def initialize(method_name, invocations, with_filter, times_predicate)
23
+ self.method_name = method_name
24
+ self.invocations = invocations
25
+ self.with_filter = with_filter
26
+ self.times_predicate = times_predicate
27
+ end
28
+
29
+ def get_message
30
+ raise "I should have been overridden"
31
+ end
32
+
33
+ def times_invoked
34
+ invocations.size
35
+ end
36
+
37
+ def inspect_arguments(arguments)
38
+ # can we fix this as soon as an array enters the system instead of catching it here?
39
+ if arguments.kind_of? Invocation
40
+ ArgsInspector.inspect arguments
41
+ else
42
+ ArgsInspector.inspect Invocation.new arguments
43
+ end
44
+ end
45
+
46
+ def expected_invocation
47
+ with_filter.expected_invocation
48
+ end
49
+
50
+ def times_msg(n)
51
+ "#{n} time#{'s' unless n == 1}"
52
+ end
53
+
54
+ def expected_times_invoked
55
+ times_predicate.expected_times_invoked
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,78 @@
1
+ require 'surrogate/rspec/invocation_matcher'
2
+
3
+ class Surrogate
4
+ module RSpec
5
+ class HaveBeenAskedForIts < InvocationMatcher
6
+
7
+ class FailureMessageShouldDefault < AbstractFailureMessage
8
+ def get_message
9
+ "was never asked for its #{ method_name }"
10
+ end
11
+ end
12
+
13
+ class FailureMessageShouldWith < AbstractFailureMessage
14
+ def get_message
15
+ message = "should have been asked for its #{ method_name } with #{ inspect_arguments expected_invocation }, but "
16
+ if times_invoked.zero?
17
+ message << "was never asked"
18
+ else
19
+ inspected_invocations = invocations.map { |invocation| inspect_arguments invocation }
20
+ message << "got #{inspected_invocations.join ', '}"
21
+ end
22
+ end
23
+ end
24
+
25
+ class FailureMessageShouldTimes < AbstractFailureMessage
26
+ def get_message
27
+ "should have been asked for its #{ method_name } #{ times_msg expected_times_invoked }, but was asked #{ times_msg times_invoked }"
28
+ end
29
+ end
30
+
31
+ class FailureMessageWithTimes < AbstractFailureMessage
32
+ def get_message
33
+ message = "should have been asked for its #{ method_name } #{ times_msg expected_times_invoked } with #{ inspect_arguments expected_invocation }, but "
34
+ if times_invoked.zero?
35
+ message << "was never asked"
36
+ else
37
+ message << "was asked #{times_msg times_invoked}"
38
+ end
39
+ end
40
+ end
41
+
42
+ class FailureMessageShouldNotDefault < AbstractFailureMessage
43
+ def get_message
44
+ "shouldn't have been asked for its #{ method_name }, but was asked #{ times_msg times_invoked }"
45
+ end
46
+ end
47
+
48
+ class FailureMessageShouldNotWith < AbstractFailureMessage
49
+ def get_message
50
+ message = "should not have been asked for its #{ method_name } with #{ inspect_arguments expected_invocation }, but "
51
+ if times_invoked.zero?
52
+ message << "was never asked"
53
+ else
54
+ inspected_invocations = invocations.map { |invocation| inspect_arguments invocation }
55
+ message << "got #{inspected_invocations.join ', '}"
56
+ end
57
+ end
58
+ end
59
+
60
+ class FailureMessageShouldNotTimes < AbstractFailureMessage
61
+ def get_message
62
+ "shouldn't have been asked for its #{ method_name } #{ times_msg expected_times_invoked }, but was"
63
+ end
64
+ end
65
+
66
+ class FailureMessageShouldNotWithTimes < AbstractFailureMessage
67
+ def get_message
68
+ message = "should not have been asked for its #{ method_name } #{ times_msg expected_times_invoked } with #{ inspect_arguments expected_invocation }, "
69
+ if times_invoked.zero?
70
+ message << "was never asked"
71
+ else
72
+ message << "was asked #{times_msg times_invoked}"
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,12 @@
1
+ require 'surrogate/rspec/have_been_told_to'
2
+
3
+ class Surrogate
4
+ module RSpec
5
+ class HaveBeenInitializedWith < HaveBeenToldTo
6
+ def initialize(*initialization_args, &block)
7
+ super :initialize
8
+ with *initialization_args, &block
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,77 @@
1
+ require 'surrogate/rspec/invocation_matcher'
2
+
3
+ class Surrogate
4
+ module RSpec
5
+ class HaveBeenToldTo < InvocationMatcher
6
+ class FailureMessageShouldDefault < AbstractFailureMessage
7
+ def get_message
8
+ "was never told to #{ method_name }"
9
+ end
10
+ end
11
+
12
+ class FailureMessageShouldWith < AbstractFailureMessage
13
+ def get_message
14
+ message = "should have been told to #{ method_name } with #{ inspect_arguments expected_invocation }, but "
15
+ if times_invoked.zero?
16
+ message << "was never told to"
17
+ else
18
+ inspected_invocations = invocations.map { |invocation| inspect_arguments invocation }
19
+ message << "got #{inspected_invocations.join ', '}"
20
+ end
21
+ end
22
+ end
23
+
24
+ class FailureMessageShouldTimes < AbstractFailureMessage
25
+ def get_message
26
+ "should have been told to #{ method_name } #{ times_msg expected_times_invoked } but was told to #{ method_name } #{ times_msg times_invoked }"
27
+ end
28
+ end
29
+
30
+ class FailureMessageWithTimes < AbstractFailureMessage
31
+ def get_message
32
+ message = "should have been told to #{ method_name } #{ times_msg expected_times_invoked } with #{ inspect_arguments expected_invocation }, but "
33
+ if times_invoked.zero?
34
+ message << "was never told to"
35
+ else
36
+ message << "got it #{times_msg times_invoked}"
37
+ end
38
+ end
39
+ end
40
+
41
+ class FailureMessageShouldNotDefault < AbstractFailureMessage
42
+ def get_message
43
+ "shouldn't have been told to #{ method_name }, but was told to #{ method_name } #{ times_msg times_invoked }"
44
+ end
45
+ end
46
+
47
+ class FailureMessageShouldNotWith < AbstractFailureMessage
48
+ def get_message
49
+ message = "should not have been told to #{ method_name } with #{ inspect_arguments expected_invocation }, but "
50
+ if times_invoked.zero?
51
+ message << "was never told to"
52
+ else
53
+ inspected_invocations = invocations.map { |invocation| inspect_arguments invocation }
54
+ message << "got #{inspected_invocations.join ', '}"
55
+ end
56
+ end
57
+ end
58
+
59
+ class FailureMessageShouldNotTimes < AbstractFailureMessage
60
+ def get_message
61
+ "shouldn't have been told to #{ method_name } #{ times_msg expected_times_invoked }, but was"
62
+ end
63
+ end
64
+
65
+ class FailureMessageShouldNotWithTimes < AbstractFailureMessage
66
+ def get_message
67
+ message = "should not have been told to #{ method_name } #{ times_msg expected_times_invoked } with #{ inspect_arguments expected_invocation }, but "
68
+ if times_invoked.zero?
69
+ message << "was never told to"
70
+ else
71
+ message << "got it #{times_msg times_invoked}"
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,81 @@
1
+ require 'surrogate/rspec/abstract_failure_message'
2
+ require 'surrogate/rspec/times_predicate'
3
+ require 'surrogate/rspec/with_filter'
4
+
5
+ class Surrogate
6
+ module RSpec
7
+ class InvocationMatcher
8
+ attr_accessor :times_predicate, :with_filter, :surrogate, :method_name
9
+
10
+ def initialize(method_name)
11
+ self.method_name = method_name
12
+ self.times_predicate = TimesPredicate.new
13
+ self.with_filter = WithFilter.new
14
+ end
15
+
16
+ def matches?(surrogate)
17
+ self.surrogate = surrogate
18
+ times_predicate.matches? filtered_args
19
+ end
20
+
21
+ def filtered_args
22
+ @filtered_args ||= with_filter.filter invocations
23
+ end
24
+
25
+ def invocations
26
+ surrogate.invocations(method_name)
27
+ end
28
+
29
+ def failure_message_for_should
30
+ raise "THIS METHOD SHOULD HAVE BEEN OVERRIDDEN"
31
+ end
32
+
33
+ def failure_message_for_should_not
34
+ raise "THIS METHOD SHOULD HAVE BEEN OVERRIDDEN"
35
+ end
36
+
37
+ def times(times_invoked)
38
+ @times_predicate = TimesPredicate.new(times_invoked, :==)
39
+ self
40
+ end
41
+
42
+ def with(*arguments, &expectation_block)
43
+ self.with_filter = WithFilter.new arguments, :args_must_match, &expectation_block
44
+ arguments << expectation_block if expectation_block
45
+ self
46
+ end
47
+
48
+ def failure_message_for_should
49
+ message_for(
50
+ if times_predicate.default? && with_filter.default?
51
+ :FailureMessageShouldDefault
52
+ elsif times_predicate.default?
53
+ :FailureMessageShouldWith
54
+ elsif with_filter.default?
55
+ :FailureMessageShouldTimes
56
+ else
57
+ :FailureMessageWithTimes
58
+ end
59
+ )
60
+ end
61
+
62
+ def failure_message_for_should_not
63
+ message_for(
64
+ if times_predicate.default? && with_filter.default?
65
+ :FailureMessageShouldNotDefault
66
+ elsif times_predicate.default?
67
+ :FailureMessageShouldNotWith
68
+ elsif with_filter.default?
69
+ :FailureMessageShouldNotTimes
70
+ else
71
+ :FailureMessageShouldNotWithTimes
72
+ end
73
+ )
74
+ end
75
+
76
+ def message_for(failure_class_name)
77
+ self.class.const_get(failure_class_name).new(method_name, invocations, with_filter, times_predicate).get_message
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,19 @@
1
+ class Surrogate
2
+ module RSpec
3
+ class TimesPredicate
4
+ attr_accessor :expected_times_invoked, :comparer
5
+ def initialize(expected_times_invoked=0, comparer=:<)
6
+ self.expected_times_invoked = expected_times_invoked
7
+ self.comparer = comparer
8
+ end
9
+
10
+ def matches?(invocations)
11
+ expected_times_invoked.send comparer, invocations.size
12
+ end
13
+
14
+ def default?
15
+ expected_times_invoked == 0 && comparer == :<
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,123 @@
1
+ class Surrogate
2
+ module RSpec
3
+ class WithFilter
4
+
5
+ class BlockAsserter
6
+ def initialize(definition_block)
7
+ @call_with = Invocation.new []
8
+ definition_block.call self
9
+ end
10
+
11
+ def call_with(*args, &block)
12
+ @call_with = Invocation.new args, &block
13
+ end
14
+
15
+ def returns(value=nil, &block)
16
+ @returns = block || lambda { |returned| returned.should == value }
17
+ end
18
+
19
+ def before(&block)
20
+ @before = block
21
+ end
22
+
23
+ def after(&block)
24
+ @after = block
25
+ end
26
+
27
+ def arity(n)
28
+ @arity = n
29
+ end
30
+
31
+ def matches?(block_to_test)
32
+ matches = before_matches? block_to_test
33
+ matches &&= return_value_matches? block_to_test
34
+ matches &&= arity_matches? block_to_test
35
+ matches &&= after_matches? block_to_test
36
+ matches
37
+ end
38
+
39
+ private
40
+
41
+ # matches if no return specified, or returned value == specified value
42
+ def return_value_matches?(block_to_test)
43
+ returned_value = block_to_test.call(*@call_with.args, &@call_with.block)
44
+ @returns.call returned_value if @returns
45
+ true
46
+ rescue ::RSpec::Expectations::ExpectationNotMetError
47
+ false
48
+ end
49
+
50
+ # matches if the first time it is called, it raises nothing
51
+ def before_matches?(*)
52
+ @before_has_been_invoked || (@before && @before.call)
53
+ ensure
54
+ return @before_has_been_invoked = true unless $!
55
+ end
56
+
57
+ # matches if nothing is raised
58
+ def after_matches?(block_to_test)
59
+ @after && @after.call
60
+ true
61
+ end
62
+
63
+ def arity_matches?(block_to_test)
64
+ return true unless @arity
65
+ block_to_test.arity == @arity
66
+ end
67
+
68
+ attr_accessor :block_to_test
69
+ end
70
+
71
+
72
+
73
+
74
+ # rename args to invocation
75
+ attr_accessor :expected_invocation, :block, :pass, :filter_name
76
+
77
+ def initialize(args=[], filter_name=:default_filter, &block)
78
+ self.expected_invocation = Invocation.new args.dup, &block
79
+ self.block = block
80
+ self.pass = send filter_name
81
+ self.filter_name = filter_name
82
+ end
83
+
84
+ def filter(invocations)
85
+ invocations.select &pass
86
+ end
87
+
88
+ def default?
89
+ filter_name == :default_filter
90
+ end
91
+
92
+ private
93
+
94
+ def default_filter
95
+ Proc.new { true }
96
+ end
97
+
98
+ def args_must_match
99
+ lambda { |invocation| args_match?(invocation) && blocks_match?(invocation) }
100
+ end
101
+
102
+ def blocks_match?(actual_invocation)
103
+ # surely this is wrong
104
+ return true unless expected_invocation.has_block?
105
+ return unless actual_invocation.has_block?
106
+ block_asserter.matches? actual_invocation.block
107
+ end
108
+
109
+ def block_asserter
110
+ @block_asserter ||= BlockAsserter.new expected_invocation.block
111
+ end
112
+
113
+ def args_match?(actual_invocation)
114
+ if RSpec.rspec_mocks_loaded?
115
+ rspec_arg_expectation = ::RSpec::Mocks::ArgumentExpectation.new *expected_invocation.args
116
+ rspec_arg_expectation.args_match? *actual_invocation.args
117
+ else
118
+ expected_arguments == actual_arguments
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end