not_a_mock 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ rdoc
2
+ coverage
3
+ pkg
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 Peter Yandell
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,82 @@
1
+ = Not A Mock
2
+
3
+ A cleaner and DRYer alternative to mocking and stubbing with RSpec.
4
+
5
+ http://notahat.com/not_a_mock
6
+
7
+ == A Quick Introduction
8
+
9
+ === Mocking (Not)
10
+
11
+ When you're setting up for a spec, you can ask that method calls on an object be recorded:
12
+
13
+ object.track_methods(:name, :length)
14
+
15
+ Once your code has run, you can make assertions about what methods were called, what arguments
16
+ they took, their results, etc.
17
+
18
+ object.should have_received(:length).without_args.and_returned(42)
19
+ object.should have_received(:name).twice
20
+
21
+ See NotAMock::Matchers for an explanation of the available assertions, plus
22
+ Object#track_methods and Object#untrack_methods.
23
+
24
+ === Stubbing
25
+
26
+ ==== Stubbing Methods
27
+
28
+ You can replace a method on an object with a stub version like this:
29
+
30
+ object.stub_method(:method => return_value)
31
+
32
+ Any call to +method+ after this will return +return_value+ without invoking
33
+ the method's usual code.
34
+
35
+ Calls to stub methods are recorded as if you had called +track_methods+
36
+ on them, so you can make assertions about them as shown above.
37
+
38
+ See Object#stub_methods, Object#unstub_methods.
39
+
40
+ ==== Stubbing Objects
41
+
42
+ You can also replace an entire object with a stub version like this:
43
+
44
+ my_object = MyClass.stub_instance(:method_a => return_value, :method_b => return_value, ...)
45
+
46
+ The returned +my_object+ is a stub instance of MyClass with the given methods
47
+ defined to provide the corresponding return values.
48
+
49
+ See Object.stub_instance.
50
+
51
+ ==== Stubbing ActiveRecord Instances
52
+
53
+ When you call +stub_instance+ on an ActiveRecord::Base subclass,
54
+ Not A Mock automatically provides an +id+ method and generates an
55
+ id for the object.
56
+
57
+ == Installation
58
+
59
+ (The following describes using NotAMock with Rails. It should also be possible
60
+ to use it outside of Rails, but I haven't tried it.)
61
+
62
+ First, install the rspec and rspec_on_rails plugins:
63
+
64
+ ruby script/plugin install http://rspec.rubyforge.org/svn/tags/CURRENT/rspec
65
+ ruby script/plugin install http://rspec.rubyforge.org/svn/tags/CURRENT/rspec_on_rails
66
+ ruby script/generate rspec
67
+
68
+ (See http://rspec.info/documentation/rails/install.html for more details.)
69
+
70
+ Second, install the not_a_mock plugin:
71
+
72
+ ruby script/plugin install git://github.com/notahat/not_a_mock.git
73
+
74
+ Finally, add the following to your project's spec/spec_helper.rb:
75
+
76
+ config.mock_with NotAMock::RspecMockFrameworkAdapter
77
+
78
+ == Contributing
79
+
80
+ Send bugs, patches, and suggestions to Pete Yandell (pete@notahat.com)
81
+
82
+ Thanks to Pat Allan and Steve Hayes for contributing patches.
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ require 'rake'
2
+ require 'spec/rake/spectask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc "Default: run specs"
6
+ task :default => :spec
7
+
8
+ desc "Run all the specs for the notamock plugin."
9
+ Spec::Rake::SpecTask.new do |t|
10
+ t.spec_files = FileList['spec/**/*_spec.rb']
11
+ t.spec_opts = ['--colour']
12
+ t.rcov = true
13
+ end
14
+
15
+ desc "Generate documentation for the notamock plugin."
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'NotAMock'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README.rdoc')
21
+ rdoc.rdoc_files.include('MIT-LICENSE')
22
+ rdoc.rdoc_files.include('TODO')
23
+ rdoc.rdoc_files.include('lib/**/*.rb')
24
+ end
25
+
26
+ begin
27
+ require 'jeweler'
28
+ Jeweler::Tasks.new do |gemspec|
29
+ gemspec.name = "not_a_mock"
30
+ gemspec.summary = "A cleaner and DRYer alternative to mocking and stubbing with RSpec"
31
+ gemspec.email = "pete@notahat.com"
32
+ gemspec.homepage = "http://notahat.com/not_a_mock"
33
+ gemspec.authors = ["Pete Yandell"]
34
+ end
35
+ Jeweler::GemcutterTasks.new
36
+ rescue LoadError
37
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
38
+ end
data/TODO ADDED
@@ -0,0 +1,49 @@
1
+ == To Do
2
+
3
+ === Recording calls that throw exceptions
4
+
5
+ Currently calls to methods that throw exceptions are not recorded. It would
6
+ be good if they were, and if assertions could be made against them, e.g.
7
+
8
+ object.should have_received(:gsub).and_raised(ArgumentError)
9
+
10
+ === Stubbing and recording of methods that take blocks
11
+
12
+ You can't stub or record calls for methods that take blocks. This is very
13
+ difficult to fix in Ruby 1.8, but will be much easier in 1.9.
14
+
15
+ === Better Error Messages
16
+
17
+ Error messages are pretty smart already. If I call:
18
+
19
+ object.should have_received(:gsub).with("Hello", "Goodbye").and_returned("Goodbye, world!")
20
+
21
+ I might get an error like:
22
+
23
+ Object received gsub, but not with arguments ["Hello", "Goodbye"].
24
+
25
+ It would be much more useful if the error message told you the arguments
26
+ it actually received as well.
27
+
28
+ === Order Assertions
29
+
30
+ I should be able to write something like this to assert that the calls
31
+ happened in order:
32
+
33
+ in_order do
34
+ object_a.should have_received(:message_a)
35
+ object_b.should have_received(:message_b)
36
+ object_a.should have_received(:message_c)
37
+ end
38
+
39
+ === Smarter ActiveRecord stubbing
40
+
41
+ I often find myself doing something like this before a test:
42
+
43
+ @comment = Comment.stub_object(:body => "Great!")
44
+ @comments = Object.stub_object(:find => [@comment])
45
+ @article = Article.stub_object(:title => "Hello, world!", :comments => @comments)
46
+ Article.stub_method(:find => @article)
47
+
48
+ I'd like to find a way to make this neater, auto-generate the stub
49
+ relationship, etc.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,12 @@
1
+ module ActiveRecord # :nodoc:
2
+ class Base
3
+
4
+ def self.stub_instance(methods = {})
5
+ @@__stub_object_id ||= 1000
6
+ @@__stub_object_id += 1
7
+ methods = methods.merge(:id => @@__stub_object_id, :to_param => @@__stub_object_id.to_s)
8
+ NotAMock::Stub.new(self, methods)
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,32 @@
1
+ require 'set'
2
+
3
+ module Spec #:nodoc:
4
+ module Mocks #:nodoc:
5
+ class AnyOrderArgConstraint #:nodoc:
6
+ def initialize(array)
7
+ @array = array
8
+ end
9
+
10
+ def ==(arg)
11
+ Set.new(@array) == Set.new(arg)
12
+ end
13
+
14
+ def inspect
15
+ "in_any_order(#{@array.inspect})"
16
+ end
17
+ end
18
+
19
+ module ArgumentMatchers #:nodoc:
20
+ class AnyArgMatcher #:nodoc:
21
+ def inspect
22
+ 'anything'
23
+ end
24
+ end
25
+
26
+ def in_any_order(array)
27
+ Spec::Mocks::AnyOrderArgConstraint.new(array)
28
+ end
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,88 @@
1
+ require 'singleton'
2
+ require 'not_a_mock/object_extensions'
3
+
4
+ module NotAMock
5
+ # The CallRecorder is a singleton that keeps track of all the call
6
+ # recording hooks installed, and keeps a central record of calls.
7
+ class CallRecorder
8
+ include Singleton
9
+
10
+ def initialize
11
+ @calls = []
12
+ @tracked_methods = []
13
+ end
14
+
15
+ # An array of recorded calls in chronological order.
16
+ #
17
+ # Each call is represented by a hash, in this format:
18
+ # { :object => example_object, :method => :example_method, :args => ["example argument"], :result => "example result" }
19
+ attr_reader :calls
20
+
21
+ # Return an array of all the calls made to any method of +object+, in chronological order.
22
+ def calls_by_object(object)
23
+ @calls.select {|call| call[:object] == object }
24
+ end
25
+
26
+ # Return an array of all the calls made to +method+ of +object+, in chronological order.
27
+ def calls_by_object_and_method(object, method)
28
+ @calls.select {|call| call[:object] == object && call[:method] == method }
29
+ end
30
+
31
+ # Patch +object+ so that future calls to +method+ will be recorded.
32
+ #
33
+ # You should call Object#track_methods rather than calling this directly.
34
+ def track_method(object, method) #:nodoc:
35
+ unless @tracked_methods.include?([object, method])
36
+ @tracked_methods << [object, method]
37
+ add_hook(object, method)
38
+ end
39
+ end
40
+
41
+ # Remove the patch from +object+ so that future calls to +method+
42
+ # will not be recorded.
43
+ #
44
+ # You should call Object#track_methods rather than calling this directly.
45
+ def untrack_method(object, method) #:nodoc:
46
+ if @tracked_methods.delete([object, method])
47
+ remove_hook(object, method)
48
+ end
49
+ end
50
+
51
+ # Stop recording all calls.
52
+ def untrack_all
53
+ @tracked_methods.each do |object, method|
54
+ remove_hook(object, method)
55
+ end
56
+ @tracked_methods = []
57
+ end
58
+
59
+ # Remove all patches so that calls are no longer recorded, and clear the call log.
60
+ def reset
61
+ untrack_all
62
+ @calls = []
63
+ end
64
+
65
+ private
66
+
67
+ def add_hook(object, method)
68
+ object.meta_eval do
69
+ alias_method("__unlogged_#{method}", method)
70
+ define_method(method) {|*args| CallRecorder.instance.send(:record_and_send, self, method, args) }
71
+ end
72
+ end
73
+
74
+ def record_and_send(object, method, args)
75
+ result = object.send("__unlogged_#{method}", *args)
76
+ @calls << { :object => object, :method => method, :args => args, :result => result }
77
+ result
78
+ end
79
+
80
+ def remove_hook(object, method)
81
+ object.meta_eval do
82
+ alias_method(method, "__unlogged_#{method}")
83
+ remove_method("__unlogged_#{method}")
84
+ end
85
+ end
86
+
87
+ end
88
+ end
@@ -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,68 @@
1
+ module NotAMock
2
+ # == Message Assertions
3
+ #
4
+ # You can assert that an object should have received particular messages:
5
+ #
6
+ # object.should have_received(:message)
7
+ # object.should_not have_received(:message)
8
+ #
9
+ # Further restrictions cannot be added after +should_not have_received+.
10
+ #
11
+ # You can also make general assertions about whether an object should have
12
+ # received any messages:
13
+ #
14
+ # object.should have_been_called
15
+ # object.should_not have_been_called
16
+ #
17
+ # == Argument Assertions
18
+ #
19
+ # You can assert that a call was made with or without arguments:
20
+ #
21
+ # object.should have_received(:message).with(arg1, arg2, ...)
22
+ # object.should have_received(:message).without_args
23
+ #
24
+ # See NotAMock::Matchers::ArgsMatcher for more information.
25
+ #
26
+ # == Return Value Assertions
27
+ #
28
+ # object.should have_received(:message).and_returned(return_value)
29
+ # object.should have_received(:message).with(arg1, arg2, ...).and_returned(return_value)
30
+ #
31
+ # == Count Assertions
32
+ #
33
+ # object.should have_received(:message).once
34
+ # object.should have_received(:message).with(arg1, arg2, ...).twice
35
+ # object.should have_received(:message).with(arg1, arg2, ...).and_returned(return_value).exactly(n).times
36
+ # object.should have_been_called.exactly(n).times
37
+ #
38
+ # Any count specifier must go at the end of the expression.
39
+ #
40
+ # The exception to this rule is +once+, which allows things like this:
41
+ #
42
+ # object.should have_received(:message).once.with(arg1, arg2, ...).and_returned(return_value)
43
+ #
44
+ # which verifies that +message+ was called only once, and that call had
45
+ # the given arguments and return value.
46
+ #
47
+ # Note that this is subtly different from:
48
+ #
49
+ # object.should have_received(:message).with(arg1, arg2, ...).and_returned(return_value).once
50
+ #
51
+ # which verifies that +message+ was called with the given arguments and
52
+ # return value only once. It may, however, have been called with different
53
+ # arguments and return value at other times.
54
+ #
55
+ # == Assertion Failures
56
+ #
57
+ # FIXME: Write some docs about the nice error messages.
58
+ module Matchers
59
+ end
60
+ end
61
+
62
+ def have_been_called
63
+ NotAMock::Matchers::AnythingMatcher.new
64
+ end
65
+
66
+ def have_received(method)
67
+ NotAMock::Matchers::MethodMatcher.new(method)
68
+ end