not_a_mock 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,105 @@
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
+ case method
38
+ when Symbol
39
+ NotAMock::CallRecorder.instance.untrack_method(self, method)
40
+ NotAMock::Stubber.instance.unstub_method(self, method)
41
+ NotAMock::Stubber.instance.stub_method(self, method, &block)
42
+ NotAMock::CallRecorder.instance.track_method(self, method)
43
+ when Hash
44
+ stub_methods(method)
45
+ else
46
+ raise ArgumentError
47
+ end
48
+ end
49
+
50
+ # Takes a hash of method names mapped to results, and replaces each named
51
+ # method on this object with a stub version returning the corresponding result.
52
+ #
53
+ # Calls to stubbed methods are recorded in the NotAMock::CallRecorder,
54
+ # so you can later make assertions about them as described in
55
+ # NotAMock::Matchers.
56
+ def stub_methods(methods)
57
+ methods.each do |method, result|
58
+ stub_method(method) {|*args| result }
59
+ end
60
+ end
61
+
62
+ # Takes a hash of method names mapped to exceptions, and replaces each named
63
+ # method on this object with a stub version returning the corresponding exception.
64
+ def stub_methods_to_raise(methods)
65
+ methods.each do |method, exception|
66
+ stub_method(method) {|*args| raise exception }
67
+ end
68
+ end
69
+ alias_method(:stub_method_to_raise, :stub_methods_to_raise)
70
+
71
+ # Removes the stubbed versions of the given methods and restores the
72
+ # original methods.
73
+ def unstub_methods(*methods)
74
+ methods.each do |method, result|
75
+ NotAMock::CallRecorder.instance.untrack_method(self, method)
76
+ NotAMock::Stubber.instance.unstub_method(self, method)
77
+ end
78
+ end
79
+ alias_method(:unstub_method, :unstub_methods)
80
+
81
+ class << self
82
+ # Called on a class, creates a stub instance of that class. Takes a hash of
83
+ # method names and their returns values, and creates those methods on the new
84
+ # stub instance.
85
+ #
86
+ # See NotAMock::Stub for more details about the returned objects.
87
+ def stub_instance(methods = {})
88
+ NotAMock::Stub.new(self, methods)
89
+ end
90
+ end
91
+
92
+ # Returns the metaclass of this object. For an explanation of metaclasses, see:
93
+ # http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html
94
+ def metaclass
95
+ class << self
96
+ self
97
+ end
98
+ end
99
+
100
+ # Evaluates the block in the context of this object's metaclass.
101
+ def meta_eval(&block)
102
+ metaclass.instance_eval(&block)
103
+ end
104
+
105
+ 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,69 @@
1
+ require 'singleton'
2
+ require 'not_a_mock/object_extensions'
3
+
4
+ module NotAMock
5
+ # The Stubber is a singleton that keeps track of all the stub methods
6
+ # installed in any object.
7
+ class Stubber
8
+ include Singleton
9
+
10
+ def initialize
11
+ @stubbed_methods = []
12
+ end
13
+
14
+ # Stub +method+ on +object+ to evalutate +block+ and return the result.
15
+ #
16
+ # You should call Object#stub_method rathing than calling this directly.
17
+ def stub_method(object, method, &block) #:nodoc:
18
+ unless @stubbed_methods.include?([object, method])
19
+ @stubbed_methods << [object, method]
20
+ add_hook(object, method, &block)
21
+ end
22
+ end
23
+
24
+ # Remove the stubbed +method+ on +object+.
25
+ #
26
+ # You should call Object#unstub_methods rather than calling this directly.
27
+ def unstub_method(object, method) #:nodoc:
28
+ if @stubbed_methods.delete([object, method])
29
+ remove_hook(object, method)
30
+ end
31
+ end
32
+
33
+ # Removes all stub methods.
34
+ def reset
35
+ @stubbed_methods.each do |object, method|
36
+ remove_hook(object, method)
37
+ end
38
+ @stubbed_methods = []
39
+ end
40
+
41
+ private
42
+
43
+ def add_hook(object, method, &block)
44
+ method_exists = method_at_any_level?(object, method.to_s)
45
+ object.meta_eval do
46
+ alias_method("__unstubbed_#{method}", method) if method_exists
47
+ define_method(method, &block)
48
+ end
49
+ end
50
+
51
+ def remove_hook(object, method)
52
+ method_exists = method_at_any_level?(object, "__unstubbed_#{method}")
53
+ object.meta_eval do
54
+ if method_exists
55
+ alias_method(method, "__unstubbed_#{method}")
56
+ remove_method("__unstubbed_#{method}")
57
+ else
58
+ remove_method(method)
59
+ end
60
+ end
61
+ end
62
+
63
+ def method_at_any_level?(object, method)
64
+ object.methods.include?(method) ||
65
+ object.protected_methods.include?(method) ||
66
+ object.private_methods.include?(method)
67
+ end
68
+ end
69
+ end
data/lib/not_a_mock.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'not_a_mock/active_record_extensions'
2
+ require 'not_a_mock/argument_constraint_extensions'
3
+ require 'not_a_mock/call_recorder'
4
+ require 'not_a_mock/matchers'
5
+ require 'not_a_mock/matchers/anything_matcher'
6
+ require 'not_a_mock/matchers/args_matcher'
7
+ require 'not_a_mock/matchers/call_matcher'
8
+ require 'not_a_mock/matchers/method_matcher'
9
+ require 'not_a_mock/matchers/result_matcher'
10
+ require 'not_a_mock/matchers/times_matcher'
11
+ require 'not_a_mock/object_extensions'
12
+ require 'not_a_mock/rspec_mock_framework_adapter'
13
+ require 'not_a_mock/stubber'
14
+ require 'not_a_mock/stub'
@@ -0,0 +1,68 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'not_a_mock'
3
+
4
+ class TrackedClass
5
+ def my_method(argument)
6
+ "result"
7
+ end
8
+ end
9
+
10
+ describe "A recorded method" do
11
+
12
+ before do
13
+ @recorder = NotAMock::CallRecorder.instance
14
+ @object = TrackedClass.new
15
+ @object.track_method(:my_method)
16
+ end
17
+
18
+ it "should record a method call" do
19
+ @object.my_method("argument")
20
+
21
+ @recorder.calls.should include(:object => @object, :method => :my_method, :args => ["argument"], :result => "result")
22
+ end
23
+
24
+ it "should not record calls after untrack_method" do
25
+ @object.my_method("argument 1")
26
+ @object.untrack_method(:my_method)
27
+ @object.my_method("argument 2")
28
+
29
+ @recorder.calls.should include(:object => @object, :method => :my_method, :args => ["argument 1"], :result => "result")
30
+ @recorder.calls.should_not include(:object => @object, :method => :my_method, :args => ["argument 2"], :result => "result")
31
+ end
32
+
33
+ it "should not record calls after stop_all_recording" do
34
+ @object.track_method(:my_method)
35
+ @object.my_method("argument 1")
36
+ @recorder.untrack_all
37
+ @object.my_method("argument 2")
38
+
39
+ @recorder.calls.should include(:object => @object, :method => :my_method, :args => ["argument 1"], :result => "result")
40
+ @recorder.calls.should_not include(:object => @object, :method => :my_method, :args => ["argument 2"], :result => "result")
41
+ end
42
+
43
+ it "should not break when track_method is called again" do
44
+ @object.track_method(:my_method)
45
+ @object.my_method("argument 1")
46
+ @object.untrack_method(:my_method)
47
+ @object.my_method("argument 2")
48
+
49
+ @recorder.calls.should include(:object => @object, :method => :my_method, :args => ["argument 1"], :result => "result")
50
+ @recorder.calls.should_not include(:object => @object, :method => :my_method, :args => ["argument 2"], :result => "result")
51
+ end
52
+
53
+ it "should not break when untrack_method is called more than once" do
54
+ @object.track_method(:my_method)
55
+ @object.my_method("argument 1")
56
+ @object.untrack_method(:my_method)
57
+ @object.untrack_method(:my_method)
58
+ @object.my_method("argument 2")
59
+
60
+ @recorder.calls.should include(:object => @object, :method => :my_method, :args => ["argument 1"], :result => "result")
61
+ @recorder.calls.should_not include(:object => @object, :method => :my_method, :args => ["argument 2"], :result => "result")
62
+ end
63
+
64
+ after do
65
+ @recorder.reset
66
+ end
67
+
68
+ end
@@ -0,0 +1,217 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'not_a_mock'
3
+
4
+ class TrackedClass < Object
5
+ def initialize(*calls)
6
+ calls.each do |call|
7
+ NotAMock::CallRecorder.instance.calls << call.merge(:object => self)
8
+ end
9
+ end
10
+
11
+ def inspect
12
+ "TrackedClass"
13
+ end
14
+ end
15
+
16
+ describe NotAMock::Matchers::AnythingMatcher do
17
+
18
+ it "should match if a method was called" do
19
+ @object = TrackedClass.new({ :method => :length, :args => [], :result => nil })
20
+ @matcher = have_been_called
21
+ @matcher.matches?(@object).should be_true
22
+ @matcher.negative_failure_message.should == "TrackedClass was called"
23
+ end
24
+
25
+ it "should not match if a method wasn't called" do
26
+ @object = TrackedClass.new
27
+ @matcher = have_been_called
28
+ @matcher.matches?(@object).should be_false
29
+ @matcher.failure_message.should == "TrackedClass was never called"
30
+ end
31
+
32
+ after do
33
+ NotAMock::CallRecorder.instance.reset
34
+ end
35
+
36
+ end
37
+
38
+ describe NotAMock::Matchers::MethodMatcher do
39
+
40
+ it "should match a called method" do
41
+ @object = TrackedClass.new({ :method => :length, :args => [], :result => nil })
42
+ @matcher = have_received(:length)
43
+ @matcher.matches?(@object).should be_true
44
+ @matcher.negative_failure_message.should == "TrackedClass received length"
45
+ end
46
+
47
+ it "should not match an uncalled method" do
48
+ @object = TrackedClass.new
49
+ @matcher = have_received(:width)
50
+ @matcher.matches?(@object).should be_false
51
+ @matcher.failure_message.should == "TrackedClass didn't receive width"
52
+ end
53
+
54
+ after do
55
+ NotAMock::CallRecorder.instance.reset
56
+ end
57
+
58
+ end
59
+
60
+ describe NotAMock::Matchers::ArgsMatcher, "matching calls with arguments " do
61
+
62
+ before do
63
+ @object = TrackedClass.new({ :method => :length, :args => [1, [2, 3, 4], 5], :result => nil })
64
+ end
65
+
66
+ it "should match a called method with the correct arguments" do
67
+ @matcher = have_received(:length).with(1, [2, 3, 4], 5)
68
+ @matcher.matches?(@object).should be_true
69
+ @matcher.negative_failure_message.should == "TrackedClass received length, with args [1, [2, 3, 4], 5]"
70
+ end
71
+
72
+ it "should not match a called method with the wrong arguments" do
73
+ @matcher = have_received(:length).with(3, 2, 1)
74
+ @matcher.matches?(@object).should be_false
75
+ @matcher.failure_message.should == "TrackedClass received length, but not with args [3, 2, 1]"
76
+ end
77
+
78
+ it "should match a called method with a wildcard argument" do
79
+ @matcher = have_received(:length).with(1, anything, 5)
80
+ @matcher.matches?(@object).should be_true
81
+ @matcher.matches?(@object).should be_true
82
+ @matcher.negative_failure_message.should == "TrackedClass received length, with args [1, anything, 5]"
83
+ end
84
+
85
+ it "should match a called method with a wildcard argument" do
86
+ @matcher = have_received(:length).with(1, in_any_order([4, 3, 2]), 5)
87
+ @matcher.matches?(@object).should be_true
88
+ @matcher.matches?(@object).should be_true
89
+ @matcher.negative_failure_message.should == "TrackedClass received length, with args [1, in_any_order([4, 3, 2]), 5]"
90
+ end
91
+
92
+ after do
93
+ NotAMock::CallRecorder.instance.reset
94
+ end
95
+
96
+ end
97
+
98
+ describe NotAMock::Matchers::ArgsMatcher, "matching calls without arguments" do
99
+
100
+ before do
101
+ @object = TrackedClass.new(
102
+ { :method => :length, :args => [1, 2, 3], :result => nil },
103
+ { :method => :width, :args => [], :result => nil }
104
+ )
105
+ end
106
+
107
+ it "should match a method called without arguments" do
108
+ @matcher = have_received(:width).without_args
109
+ @matcher.matches?(@object).should be_true
110
+ @matcher.negative_failure_message.should == "TrackedClass received width, without args"
111
+ end
112
+
113
+ it "should not match a method called with arguments" do
114
+ @matcher = have_received(:length).without_args
115
+ @matcher.matches?(@object).should be_false
116
+ @matcher.failure_message.should == "TrackedClass received length, but not without args"
117
+ end
118
+
119
+ after do
120
+ NotAMock::CallRecorder.instance.reset
121
+ end
122
+
123
+ end
124
+
125
+ describe NotAMock::Matchers::ResultMatcher do
126
+
127
+ before do
128
+ @object = TrackedClass.new(
129
+ { :method => :length, :args => [], :result => 13 },
130
+ { :method => :width, :args => [], :result => 42 }
131
+ )
132
+ end
133
+
134
+ it "should match a method that returned the correct result" do
135
+ @matcher = have_received(:length).and_returned(13)
136
+ @matcher.matches?(@object).should be_true
137
+ @matcher.negative_failure_message.should == "TrackedClass received length, and returned 13"
138
+ end
139
+
140
+ it "should not match a method the returned the wrong result" do
141
+ @matcher = have_received(:width).and_returned(13)
142
+ @matcher.matches?(@object).should be_false
143
+ @matcher.failure_message.should == "TrackedClass received width, but didn't return 13"
144
+ end
145
+
146
+ after do
147
+ NotAMock::CallRecorder.instance.reset
148
+ end
149
+
150
+ end
151
+
152
+ describe NotAMock::Matchers::TimesMatcher do
153
+
154
+ before do
155
+ @object = TrackedClass.new(
156
+ { :method => :once, :args => [], :result => nil },
157
+ { :method => :twice, :args => [], :result => nil },
158
+ { :method => :twice, :args => [], :result => nil },
159
+ { :method => :thrice, :args => [], :result => nil },
160
+ { :method => :thrice, :args => [], :result => nil },
161
+ { :method => :thrice, :args => [], :result => nil }
162
+ )
163
+ end
164
+
165
+ it "should match a method that was called once" do
166
+ @matcher = have_received(:once).once
167
+ @matcher.matches?(@object).should be_true
168
+ @matcher.negative_failure_message.should == "TrackedClass received once, once"
169
+ end
170
+
171
+ it "should match a method that was called twice" do
172
+ @matcher = have_received(:twice).twice
173
+ @matcher.matches?(@object).should be_true
174
+ @matcher.negative_failure_message.should == "TrackedClass received twice, twice"
175
+ end
176
+
177
+ it "should match a method that was called 3 times" do
178
+ @matcher = have_received(:thrice).exactly(3).times
179
+ @matcher.matches?(@object).should be_true
180
+ @matcher.negative_failure_message.should == "TrackedClass received thrice, 3 times"
181
+ end
182
+
183
+ it "should not match a method a method that was called the wrong number of times" do
184
+ @matcher = have_received(:thrice).once
185
+ @matcher.matches?(@object).should be_false
186
+ @matcher.failure_message.should == "TrackedClass received thrice, but 3 times"
187
+ end
188
+
189
+ after do
190
+ NotAMock::CallRecorder.instance.reset
191
+ end
192
+
193
+ end
194
+
195
+ describe "A chain of matchers" do
196
+
197
+ before do
198
+ @object = TrackedClass.new({ :method => :length, :args => [1, 2, 3], :result => 42 })
199
+ end
200
+
201
+ it "should match the correct method, args, and result" do
202
+ @matcher = have_received(:length).with(1, 2, 3).and_returned(42)
203
+ @matcher.matches?(@object).should be_true
204
+ @matcher.negative_failure_message.should == "TrackedClass received length, with args [1, 2, 3], and returned 42"
205
+ end
206
+
207
+ it "should not match the correct method, but with the incorrect args" do
208
+ @matcher = have_received(:length).with(3, 2, 1).and_returned(42)
209
+ @matcher.matches?(@object).should be_false
210
+ @matcher.failure_message.should == "TrackedClass received length, but not with args [3, 2, 1]"
211
+ end
212
+
213
+ after do
214
+ NotAMock::CallRecorder.instance.reset
215
+ end
216
+
217
+ end
@@ -0,0 +1,29 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'not_a_mock'
3
+
4
+ class ExampleActiveRecord < ActiveRecord::Base
5
+ end
6
+
7
+ describe "A stubbed ActiveRecord object" do
8
+
9
+ before do
10
+ @example = ExampleActiveRecord.stub_instance
11
+ end
12
+
13
+ it "should return a valid id" do
14
+ lambda { @example.id }.should_not raise_error(NoMethodError)
15
+ @example.id.should be_an_instance_of(Fixnum)
16
+ end
17
+
18
+ it "should return the id as a string for to_param" do
19
+ lambda { @example.to_param }.should_not raise_error(NoMethodError)
20
+ @example.to_param.should be_an_instance_of(String)
21
+ @example.to_param.should == @example.id.to_s
22
+ end
23
+
24
+ after do
25
+ NotAMock::CallRecorder.instance.reset
26
+ NotAMock::Stubber.instance.reset
27
+ end
28
+
29
+ end
@@ -0,0 +1,49 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'not_a_mock'
3
+
4
+ describe "A stub instance" do
5
+
6
+ before do
7
+ @object = String.stub_instance(:length => 42, :id => 99)
8
+ end
9
+
10
+ it "should return the right result for a stubbed method" do
11
+ @object.length.should == 42
12
+ end
13
+
14
+ it "should return its name when inspected" do
15
+ @object.inspect.should == "Stub String"
16
+ end
17
+
18
+ it "should handle the id method being stubbed" do
19
+ @object.id.should == 99
20
+ end
21
+
22
+ it "should raise an error when a method that's not stubbed is called" do
23
+ lambda { @object.whatever }.should raise_error(NoMethodError)
24
+ end
25
+
26
+ it "should record a call to a stubbed method" do
27
+ @object.length
28
+ NotAMock::CallRecorder.instance.calls.should include(:object => @object, :method => :length, :args => [], :result => 42)
29
+ end
30
+
31
+ it "should allow adding of new stubbed methods" do
32
+ @object.stub_method(:width => 7)
33
+ @object.width.should == 7
34
+ end
35
+
36
+ it "should identify itself as the underlying object" do
37
+ @object.is_a?(String).should be_true
38
+ @object.should be_kind_of(String)
39
+ @object.should be_kind_of(Object)
40
+ @object.should be_instance_of(String)
41
+ @object.class.should == String
42
+ end
43
+
44
+ after do
45
+ NotAMock::CallRecorder.instance.reset
46
+ NotAMock::Stubber.instance.reset
47
+ end
48
+
49
+ end