derickbailey-notamock 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -0,0 +1,129 @@
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
+ === Yielding To Method Blocks
58
+
59
+ There are many situation in which a stubbed method will be called with a block attached to it. For example,
60
+ if you are using the Net-SSH gem, you may have code like this:
61
+
62
+ Net::SSH.start(@server, @username, :password => @password) do |ssh|
63
+ ssh.exec!(cmd)
64
+ end
65
+
66
+ To correctly stub and test the SSH.start method and the ssh.exec! method, you need to yield an object to the
67
+ block code, that supports a method called "exec!".
68
+
69
+ To do this, you can call "yields(*args)" on a stubbed method. For example, you can can stub the SSH.start method
70
+ and the exec! method, like this:
71
+
72
+ @sshstub = Net::SSH::Connection::Session.stub_instance(:exec! => nil)
73
+ Net::SSH.stub_method(:start).yields(@sshstub)
74
+
75
+ When the .exec! method is called from withing the code block of SSH.start, the @sshstub object will be yielded to the
76
+ block, allowing you to track the method, provide a stubbed block of code to execute, etc.
77
+
78
+ In situations where you are calling .yields(*args), you can still provide a stub method block to replace the code
79
+ that is executed when the stub method is called. There are multiple ways of doing this:
80
+
81
+ Net::SSH.stub_method(:start).yields(@sshstub) { puts 'this is the method stub code for .start' }
82
+
83
+ or
84
+
85
+ stub_block = lambda { puts 'this is the method stub code for .start' }
86
+ Net::SSH.stub_method(:start, &stub_block).yields(@sshstub)
87
+
88
+ Both of these syntax forms will provide the exact same functionality - the block that puts the string of information
89
+ will be executed when you call 'Net::SSH.start'
90
+
91
+ == Installation
92
+
93
+ (The following describes using NotAMock with Rails. It should also be possible
94
+ to use it outside of Rails, but I haven't tried it.)
95
+
96
+ First, install the rspec and rspec_on_rails plugins:
97
+
98
+ ruby script/plugin install http://rspec.rubyforge.org/svn/tags/CURRENT/rspec
99
+ ruby script/plugin install http://rspec.rubyforge.org/svn/tags/CURRENT/rspec_on_rails
100
+ ruby script/generate rspec
101
+
102
+ (See http://rspec.info/documentation/rails/install.html for more details.)
103
+
104
+ Second, install the not_a_mock plugin:
105
+
106
+ ruby script/plugin install git://github.com/notahat/not_a_mock.git
107
+
108
+ Finally, add the following to your project's spec/spec_helper.rb:
109
+
110
+ config.mock_with NotAMock::RspecMockFrameworkAdapter
111
+
112
+ ==== Installation via Ruby Gems
113
+
114
+ At the moment, there is no hosted gem for not_a_mock. However, you can easily build and install the gem from the source
115
+ directly. To do so, follow these instructions:
116
+
117
+ 1. clone the source code from git://github.com/derickbailey/not_a_mock.git
118
+ 2. run 'rake jeweler:gemspec' in the root folder of your not_a_mock clone
119
+ 3. run 'rake jeweler:build' in the root folder of your not_a_mock clone
120
+ 4. run 'gem install -l pkg/NotAMock-#.#.#.gem' where #.#.# is the version number produced in step 3
121
+
122
+ After this you will be able to "include 'not_a_mock'" in your rspec tests, and configure rspec as shown for spec_helper.rb,
123
+ above.
124
+
125
+ == Contributing
126
+
127
+ Send bugs, patches, and suggestions to Pete Yandell (pete@notahat.com)
128
+
129
+ Thanks to Derick Bailey, Pat Allan and Steve Hayes for contributing patches.
@@ -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
+ namespace :jeweler do
27
+ require 'jeweler'
28
+ Jeweler::Tasks.new do |gs|
29
+ gs.name = "derickbailey-notamock"
30
+ gs.summary = "A cleaner and DRYer alternative to mocking and stubbing with RSpec"
31
+ gs.description = "A cleaner and DRYer alternative to mocking and stubbing with RSpec, using Arrange-Act-Assert syntax"
32
+ gs.homepage = "http://github.com/derickbailey/not_a_mock"
33
+ gs.authors = ["Pete Yandell", "Derick Bailey"]
34
+ gs.has_rdoc = false
35
+ gs.files.exclude("*.gemspec", ".gitignore")
36
+ gs.add_dependency('rspec', '>= 1.2.9')
37
+ end
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
+ 0.0.1
@@ -0,0 +1,15 @@
1
+ require 'not_a_mock/rspec_mock_framework_adapter'
2
+ require 'not_a_mock/active_record_extensions'
3
+ require 'not_a_mock/argument_constraint_extensions'
4
+ require 'not_a_mock/call_recorder'
5
+ require 'not_a_mock/matchers'
6
+ require 'not_a_mock/matchers/anything_matcher'
7
+ require 'not_a_mock/matchers/args_matcher'
8
+ require 'not_a_mock/matchers/call_matcher'
9
+ require 'not_a_mock/matchers/method_matcher'
10
+ require 'not_a_mock/matchers/result_matcher'
11
+ require 'not_a_mock/matchers/times_matcher'
12
+ require 'not_a_mock/object_extensions'
13
+ require 'not_a_mock/stubber'
14
+ require 'not_a_mock/stubmethod'
15
+ require 'not_a_mock/stub'
@@ -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,93 @@
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
+ end
71
+ object.instance_eval(<<-EOF, __FILE__, __LINE__)
72
+ def #{method} (*args, &block)
73
+ return_value = NotAMock::CallRecorder.instance.send(:record_and_send, self, :#{method}, args, &block)
74
+ return_value
75
+ end
76
+ EOF
77
+ end
78
+
79
+ def record_and_send(object, method, args, &block)
80
+ result = object.send("__unlogged_#{method}", *args, &block)
81
+ @calls << { :object => object, :method => method, :args => args, :result => result }
82
+ result
83
+ end
84
+
85
+ def remove_hook(object, method)
86
+ object.meta_eval do
87
+ alias_method(method, "__unlogged_#{method}")
88
+ remove_method("__unlogged_#{method}")
89
+ end
90
+ end
91
+
92
+ end
93
+ 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