derickbailey-notamock 0.0.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/MIT-LICENSE +20 -0
- data/README.rdoc +129 -0
- data/Rakefile +38 -0
- data/TODO +49 -0
- data/VERSION +1 -0
- data/lib/not_a_mock.rb +15 -0
- data/lib/not_a_mock/active_record_extensions.rb +12 -0
- data/lib/not_a_mock/argument_constraint_extensions.rb +32 -0
- data/lib/not_a_mock/call_recorder.rb +93 -0
- data/lib/not_a_mock/matchers.rb +68 -0
- data/lib/not_a_mock/matchers/anything_matcher.rb +28 -0
- data/lib/not_a_mock/matchers/args_matcher.rb +74 -0
- data/lib/not_a_mock/matchers/call_matcher.rb +64 -0
- data/lib/not_a_mock/matchers/method_matcher.rb +29 -0
- data/lib/not_a_mock/matchers/result_matcher.rb +29 -0
- data/lib/not_a_mock/matchers/times_matcher.rb +46 -0
- data/lib/not_a_mock/object_extensions.rb +107 -0
- data/lib/not_a_mock/rspec_mock_framework_adapter.rb +16 -0
- data/lib/not_a_mock/stub.rb +40 -0
- data/lib/not_a_mock/stubber.rb +85 -0
- data/lib/not_a_mock/stubmethod.rb +26 -0
- data/spec/call_recording_spec.rb +68 -0
- data/spec/matchers_spec.rb +217 -0
- data/spec/stub_active_record_spec.rb +29 -0
- data/spec/stub_instance_spec.rb +49 -0
- data/spec/stub_method_spec.rb +242 -0
- data/spec/stub_method_with_block_spec.rb +181 -0
- metadata +96 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'not_a_mock/matchers/call_matcher'
|
2
|
+
|
3
|
+
module NotAMock
|
4
|
+
module Matchers
|
5
|
+
# Matcher for
|
6
|
+
# object.should have_been_called
|
7
|
+
class AnythingMatcher < CallMatcher
|
8
|
+
|
9
|
+
def initialize(parent = nil)
|
10
|
+
super parent
|
11
|
+
end
|
12
|
+
|
13
|
+
def matches_without_parents?
|
14
|
+
@calls = CallRecorder.instance.calls_by_object(@object)
|
15
|
+
!@calls.empty?
|
16
|
+
end
|
17
|
+
|
18
|
+
def failure_message_without_parents
|
19
|
+
if matched?
|
20
|
+
" was called"
|
21
|
+
else
|
22
|
+
" was never called"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'not_a_mock/matchers/call_matcher'
|
2
|
+
|
3
|
+
module NotAMock
|
4
|
+
module Matchers
|
5
|
+
# Matcher for
|
6
|
+
# with(...)
|
7
|
+
#
|
8
|
+
# == Argument Matchers
|
9
|
+
#
|
10
|
+
# Not A Mock supports the use of RSpec's patterns for argument matching
|
11
|
+
# in mocks, and extends them. The most useful are listed below.
|
12
|
+
#
|
13
|
+
# === Anything Matcher
|
14
|
+
#
|
15
|
+
# The +anything+ pattern will match any value. For example:
|
16
|
+
#
|
17
|
+
# object.should have_received(:message).with(1, anything, 3)
|
18
|
+
#
|
19
|
+
# will match the following calls:
|
20
|
+
#
|
21
|
+
# object.message(1, 2, 3)
|
22
|
+
# object.message(1, 'Boo!', 3)
|
23
|
+
#
|
24
|
+
# but not:
|
25
|
+
#
|
26
|
+
# object.message(3, 2, 1)
|
27
|
+
# object.message(1, 2, 3, 4)
|
28
|
+
#
|
29
|
+
# === In Any Order Matcher
|
30
|
+
#
|
31
|
+
# The +in_any_order+ pattern will match an array argument, but won't care
|
32
|
+
# about order of elements in the array. For example:
|
33
|
+
#
|
34
|
+
# object.should have_received(:message).with(in_any_order([3, 2, 1]))
|
35
|
+
#
|
36
|
+
# will match the following calls:
|
37
|
+
#
|
38
|
+
# object.message([3, 2, 1])
|
39
|
+
# object.message([1, 2, 3])
|
40
|
+
#
|
41
|
+
# but not:
|
42
|
+
#
|
43
|
+
# object.message([1, 2, 3, 4])
|
44
|
+
class ArgsMatcher < CallMatcher
|
45
|
+
|
46
|
+
def initialize(args, parent = nil)
|
47
|
+
super parent
|
48
|
+
@args = args
|
49
|
+
end
|
50
|
+
|
51
|
+
def matches_without_parents?
|
52
|
+
@calls = @parent.calls.select {|entry| @args == entry[:args] }
|
53
|
+
!@calls.empty?
|
54
|
+
end
|
55
|
+
|
56
|
+
def failure_message_without_parents
|
57
|
+
if matched?
|
58
|
+
if @args.empty?
|
59
|
+
", without args"
|
60
|
+
else
|
61
|
+
", with args #{@args.inspect}"
|
62
|
+
end
|
63
|
+
else
|
64
|
+
if @args.empty?
|
65
|
+
", but not without args"
|
66
|
+
else
|
67
|
+
", but not with args #{@args.inspect}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module NotAMock
|
2
|
+
module Matchers
|
3
|
+
class CallMatcher
|
4
|
+
|
5
|
+
def initialize(parent = nil)
|
6
|
+
@parent = parent
|
7
|
+
end
|
8
|
+
|
9
|
+
def matches?(object)
|
10
|
+
@object = object
|
11
|
+
@matched = parent_matches? && matches_without_parents?
|
12
|
+
end
|
13
|
+
|
14
|
+
def matched?; @matched end
|
15
|
+
|
16
|
+
attr_reader :calls
|
17
|
+
|
18
|
+
def failure_message
|
19
|
+
if parent_matched?
|
20
|
+
parent_failure_message + failure_message_without_parents
|
21
|
+
else
|
22
|
+
parent_failure_message
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def negative_failure_message
|
27
|
+
failure_message
|
28
|
+
end
|
29
|
+
|
30
|
+
def with(*args)
|
31
|
+
ArgsMatcher.new(args, self)
|
32
|
+
end
|
33
|
+
|
34
|
+
def without_args
|
35
|
+
ArgsMatcher.new([], self)
|
36
|
+
end
|
37
|
+
|
38
|
+
def and_returned(result)
|
39
|
+
ResultMatcher.new(result, self)
|
40
|
+
end
|
41
|
+
|
42
|
+
def exactly(n)
|
43
|
+
TimesMatcher.new(n, self)
|
44
|
+
end
|
45
|
+
def once; exactly(1) end
|
46
|
+
def twice; exactly(2) end
|
47
|
+
|
48
|
+
protected
|
49
|
+
|
50
|
+
def parent_matches?
|
51
|
+
@parent.nil? || @parent.matches?(@object)
|
52
|
+
end
|
53
|
+
|
54
|
+
def parent_matched?
|
55
|
+
@parent.nil? || @parent.matched?
|
56
|
+
end
|
57
|
+
|
58
|
+
def parent_failure_message
|
59
|
+
@parent ? @parent.failure_message : @object.inspect
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'not_a_mock/matchers/call_matcher'
|
2
|
+
|
3
|
+
module NotAMock
|
4
|
+
module Matchers
|
5
|
+
# Matcher for
|
6
|
+
# object.should have_received(...)
|
7
|
+
class MethodMatcher < CallMatcher
|
8
|
+
|
9
|
+
def initialize(method, parent = nil)
|
10
|
+
super parent
|
11
|
+
@method = method
|
12
|
+
end
|
13
|
+
|
14
|
+
def matches_without_parents?
|
15
|
+
@calls = CallRecorder.instance.calls_by_object_and_method(@object, @method)
|
16
|
+
!@calls.empty?
|
17
|
+
end
|
18
|
+
|
19
|
+
def failure_message_without_parents
|
20
|
+
if matched?
|
21
|
+
" received #{@method}"
|
22
|
+
else
|
23
|
+
" didn't receive #{@method}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'not_a_mock/matchers/call_matcher'
|
2
|
+
|
3
|
+
module NotAMock
|
4
|
+
module Matchers
|
5
|
+
# Matcher for
|
6
|
+
# and_returned(...)
|
7
|
+
class ResultMatcher < CallMatcher
|
8
|
+
|
9
|
+
def initialize(result, parent = nil)
|
10
|
+
super parent
|
11
|
+
@result = result
|
12
|
+
end
|
13
|
+
|
14
|
+
def matches_without_parents?
|
15
|
+
@calls = @parent.calls.select {|entry| entry[:result] == @result }
|
16
|
+
!@calls.empty?
|
17
|
+
end
|
18
|
+
|
19
|
+
def failure_message_without_parents
|
20
|
+
if matched?
|
21
|
+
", and returned #{@result.inspect}"
|
22
|
+
else
|
23
|
+
", but didn't return #{@result.inspect}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'not_a_mock/matchers/call_matcher'
|
2
|
+
|
3
|
+
module NotAMock
|
4
|
+
module Matchers
|
5
|
+
# Matcher for +once+, +twice+, and
|
6
|
+
# exactly(n).times
|
7
|
+
class TimesMatcher < CallMatcher
|
8
|
+
|
9
|
+
def initialize(times, parent = nil)
|
10
|
+
super parent
|
11
|
+
@times = times
|
12
|
+
end
|
13
|
+
|
14
|
+
def matches_without_parents?
|
15
|
+
@calls = @parent.calls
|
16
|
+
@calls.length == @times
|
17
|
+
end
|
18
|
+
|
19
|
+
def failure_message_without_parents
|
20
|
+
if matched?
|
21
|
+
", #{times_in_english(@parent.calls.length)}"
|
22
|
+
else
|
23
|
+
", but #{times_in_english(@parent.calls.length, true)}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def times
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def times_in_english(times, only = false)
|
34
|
+
case times
|
35
|
+
when 1
|
36
|
+
only ? "only once" : "once"
|
37
|
+
when 2
|
38
|
+
"twice"
|
39
|
+
else
|
40
|
+
"#{times} times"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
class Object
|
2
|
+
|
3
|
+
# Call this on any object or class with a list of method names. Any future
|
4
|
+
# calls to those methods will be recorded in NotAMock::CallRecorder.
|
5
|
+
#
|
6
|
+
# See NotAMock::Matchers for info on how to test which methods have been
|
7
|
+
# called, with what arguments, etc.
|
8
|
+
def track_methods(*methods)
|
9
|
+
if methods.empty?
|
10
|
+
self.public_methods(false).map {|method_name| methods << method_name.to_s.to_sym}
|
11
|
+
end
|
12
|
+
|
13
|
+
methods.each do |method|
|
14
|
+
NotAMock::CallRecorder.instance.track_method(self, method)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
alias_method(:track_method, :track_methods)
|
18
|
+
alias_method(:log_calls_to, :track_methods) # For backwards compatibility.
|
19
|
+
|
20
|
+
# Stop recording calls for the given methods.
|
21
|
+
def untrack_methods(*methods)
|
22
|
+
methods.each do |method|
|
23
|
+
NotAMock::CallRecorder.instance.untrack_method(self, method)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
alias_method(:untrack_method, :untrack_methods)
|
27
|
+
|
28
|
+
# If passed a symbol and a block, this replaces the named method on this
|
29
|
+
# object with a stub version that evaluates the block and returns the result.
|
30
|
+
#
|
31
|
+
# If passed a hash, this is an alias for stub_methods.
|
32
|
+
#
|
33
|
+
# Calls to stubbed methods are recorded in the NotAMock::CallRecorder,
|
34
|
+
# so you can later make assertions about them as described in
|
35
|
+
# NotAMock::Matchers.
|
36
|
+
def stub_method(method, &block)
|
37
|
+
obj = self
|
38
|
+
case method
|
39
|
+
when Symbol
|
40
|
+
NotAMock::CallRecorder.instance.untrack_method(obj, method)
|
41
|
+
NotAMock::Stubber.instance.unstub_method(obj, method)
|
42
|
+
stubber = NotAMock::Stubber.instance.stub_method(obj, method, &block)
|
43
|
+
NotAMock::CallRecorder.instance.track_method(obj, method)
|
44
|
+
when Hash
|
45
|
+
stub_methods(method)
|
46
|
+
else
|
47
|
+
raise ArgumentError
|
48
|
+
end
|
49
|
+
stubber
|
50
|
+
end
|
51
|
+
|
52
|
+
# Takes a hash of method names mapped to results, and replaces each named
|
53
|
+
# method on this object with a stub version returning the corresponding result.
|
54
|
+
#
|
55
|
+
# Calls to stubbed methods are recorded in the NotAMock::CallRecorder,
|
56
|
+
# so you can later make assertions about them as described in
|
57
|
+
# NotAMock::Matchers.
|
58
|
+
def stub_methods(methods)
|
59
|
+
methods.each do |method, result|
|
60
|
+
stub_method(method) {|*args| result }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Takes a hash of method names mapped to exceptions, and replaces each named
|
65
|
+
# method on this object with a stub version returning the corresponding exception.
|
66
|
+
def stub_methods_to_raise(methods)
|
67
|
+
methods.each do |method, exception|
|
68
|
+
stub_method(method) {|*args| raise exception }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
alias_method(:stub_method_to_raise, :stub_methods_to_raise)
|
72
|
+
|
73
|
+
# Removes the stubbed versions of the given methods and restores the
|
74
|
+
# original methods.
|
75
|
+
def unstub_methods(*methods)
|
76
|
+
methods.each do |method, result|
|
77
|
+
NotAMock::CallRecorder.instance.untrack_method(self, method)
|
78
|
+
NotAMock::Stubber.instance.unstub_method(self, method)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
alias_method(:unstub_method, :unstub_methods)
|
82
|
+
|
83
|
+
class << self
|
84
|
+
# Called on a class, creates a stub instance of that class. Takes a hash of
|
85
|
+
# method names and their returns values, and creates those methods on the new
|
86
|
+
# stub instance.
|
87
|
+
#
|
88
|
+
# See NotAMock::Stub for more details about the returned objects.
|
89
|
+
def stub_instance(methods = {})
|
90
|
+
NotAMock::Stub.new(self, methods)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns the metaclass of this object. For an explanation of metaclasses, see:
|
95
|
+
# http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html
|
96
|
+
def metaclass
|
97
|
+
class << self
|
98
|
+
self
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Evaluates the block in the context of this object's metaclass.
|
103
|
+
def meta_eval(&block)
|
104
|
+
metaclass.instance_eval(&block)
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module NotAMock
|
2
|
+
module RspecMockFrameworkAdapter
|
3
|
+
|
4
|
+
def setup_mocks_for_rspec
|
5
|
+
end
|
6
|
+
|
7
|
+
def verify_mocks_for_rspec
|
8
|
+
end
|
9
|
+
|
10
|
+
def teardown_mocks_for_rspec
|
11
|
+
NotAMock::CallRecorder.instance.reset
|
12
|
+
NotAMock::Stubber.instance.reset
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module NotAMock
|
2
|
+
# Instances returned by Object.stub_instance are NotAMock::Stub objects.
|
3
|
+
# These do their best to masquerade as the real thing.
|
4
|
+
class Stub
|
5
|
+
|
6
|
+
# This is normall only called from Object.stub_instance.
|
7
|
+
def initialize(stubbed_class, methods = {}) #:nodoc:
|
8
|
+
@stubbed_class = stubbed_class
|
9
|
+
methods.each do |method, result|
|
10
|
+
self.meta_eval do
|
11
|
+
define_method(method) { result }
|
12
|
+
end
|
13
|
+
track_method(method)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns "Stub StubbedClass".
|
18
|
+
def inspect
|
19
|
+
"Stub #{@stubbed_class.to_s}"
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns true if the class of the stubbed object or one of its superclasses is klass.
|
23
|
+
def is_a?(klass)
|
24
|
+
@stubbed_class.ancestors.include?(klass)
|
25
|
+
end
|
26
|
+
|
27
|
+
alias_method :kind_of?, :is_a?
|
28
|
+
|
29
|
+
# Returns true if the class of the stubbed object is klass.
|
30
|
+
def instance_of?(klass)
|
31
|
+
@stubbed_class == klass
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns the class of the stubbed object.
|
35
|
+
def class
|
36
|
+
@stubbed_class
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'not_a_mock/object_extensions'
|
3
|
+
|
4
|
+
module NotAMock
|
5
|
+
|
6
|
+
# The Stubber is a singleton that keeps track of all the stub methods
|
7
|
+
# installed in any object.
|
8
|
+
class Stubber
|
9
|
+
include Singleton
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@stubbed_methods = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
# Stub +method+ on +object+ to evalutate +block+ and return the result.
|
16
|
+
#
|
17
|
+
# You should call Object#stub_method rathing than calling this directly.
|
18
|
+
def stub_method(object, method, &block) #:nodoc:
|
19
|
+
unless @stubbed_methods.include?([object, method])
|
20
|
+
stubmethod = NotAMock::StubMethod.new(&block)
|
21
|
+
@stubbed_methods[[object, method]] = stubmethod
|
22
|
+
add_hook(object, method)
|
23
|
+
end
|
24
|
+
stubmethod
|
25
|
+
end
|
26
|
+
|
27
|
+
# Remove the stubbed +method+ on +object+.
|
28
|
+
#
|
29
|
+
# You should call Object#unstub_methods rather than calling this directly.
|
30
|
+
def unstub_method(object, method) #:nodoc:
|
31
|
+
if @stubbed_methods.delete([object, method])
|
32
|
+
remove_hook(object, method)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Removes all stub methods.
|
37
|
+
def reset
|
38
|
+
@stubbed_methods.each do |key, value|
|
39
|
+
object = key[0]
|
40
|
+
method = key[1]
|
41
|
+
remove_hook(object, method)
|
42
|
+
end
|
43
|
+
@stubbed_methods = {}
|
44
|
+
end
|
45
|
+
|
46
|
+
#Retrieve the stub method data and code for stubbed +method+ on the +object+
|
47
|
+
def get_stubmethod(object, method)
|
48
|
+
@stubbed_methods[[object, method]]
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def add_hook(object, method)
|
54
|
+
method_exists = method_at_any_level?(object, method.to_s)
|
55
|
+
object.meta_eval do
|
56
|
+
alias_method("__unstubbed_#{method}", method) if method_exists
|
57
|
+
end
|
58
|
+
object.instance_eval(<<-EOF, __FILE__, __LINE__)
|
59
|
+
def #{method}(*args, &block)
|
60
|
+
stubmethod = Stubber.instance.get_stubmethod(self, :#{method})
|
61
|
+
stubmethod.yield_to_block(&block)
|
62
|
+
stubmethod.execute_return_block(*args)
|
63
|
+
end
|
64
|
+
EOF
|
65
|
+
end
|
66
|
+
|
67
|
+
def remove_hook(object, method)
|
68
|
+
method_exists = method_at_any_level?(object, "__unstubbed_#{method}")
|
69
|
+
object.meta_eval do
|
70
|
+
if method_exists
|
71
|
+
alias_method(method, "__unstubbed_#{method}")
|
72
|
+
remove_method("__unstubbed_#{method}")
|
73
|
+
else
|
74
|
+
remove_method(method)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def method_at_any_level?(object, method)
|
80
|
+
object.methods.include?(method) ||
|
81
|
+
object.protected_methods.include?(method) ||
|
82
|
+
object.private_methods.include?(method)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|