benhutton-remarkable_rails 4.0.0.alpha4

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,105 @@
1
+ * Allow should route(:get, :action => :show, :id => 1).to("/projects/1") and
2
+ should route(:get, "/projects/1").from(:action => :show, :id => 1) syntax.
3
+
4
+ * Plugin files are automatically loaded [#86]
5
+
6
+ * Allow should_route to use environment configuration [#88] (thanks to Lawrence Pit)
7
+
8
+ * Allow mock model class to be set using :as option.
9
+
10
+ * [DEPRECATION] By default all matchers perform expectations, use with_stubs => true
11
+ if you want otherwise.
12
+
13
+ * [DEPRECATION] mock_models now creates model_proc instead of mock_model.
14
+ Altough this won't fire any deprecation warning, all the documentation was changed.
15
+
16
+ * assert_valid_keys on expects
17
+
18
+ * mock_models now creates a second class method to be used on the index action [#71]
19
+ In other words, mock_models :project will create:
20
+
21
+ def self.mock_project
22
+ proc { mock_project }
23
+ end
24
+
25
+ # This was added to be used on index actions
26
+ def self.mock_projects
27
+ proc { [ mock_project ] }
28
+ end
29
+
30
+ def mock_project(stubs={})
31
+ @project ||= mock_model(Project, stubs)
32
+ end
33
+
34
+ * Allow multiple args to be given to :with in expects. If you need to verify that
35
+ an array is being sent, you need to send an array inside another array [#70]
36
+
37
+ * Allow procs or blocks to be given to respond_with_body and respond_with :body [#67]
38
+
39
+ * Allow ordered to be given to macro stubs as option [#66]
40
+
41
+ # v3.1
42
+
43
+ * Ensure set_cookies and set_session work with arrays [#55]
44
+
45
+ * Added set_cookies matcher [#51]
46
+
47
+ * Add a helper to declare that a XmlHttpRequest should be performed:
48
+
49
+ describe :get => :show do
50
+ xhr!
51
+
52
+ * Macro stubs now supports blocks too [#50]
53
+
54
+ expects :human_attribute_name, :on => Project, :with => :title do |attr|
55
+ attr.to_s.humanize
56
+ end
57
+
58
+ * :to option in set_session and set_the_flash now accepts Regexp [#46]
59
+
60
+ * render_template now works with partials [#43]
61
+
62
+ * Added to support for routing example group (inside spec/routing) [#26]
63
+
64
+ # v3.0
65
+
66
+ * redirect_to and render_template were ported from rspec-rails to
67
+ remarkable to provide I18n. The second was also extended to deal with :with,
68
+ :layout and :content_type as options.
69
+
70
+ render_with_layout, render_without_layout delegate their logic to render_template
71
+ so they share the same options.
72
+
73
+ respond_with_content_type and respond_wity_body delegate their logic to
74
+ respond_with matcher, so they also share the same options.
75
+
76
+ :set_the_flash was also redesign to inherit from :set_session, providing a
77
+ consistent API.
78
+
79
+ * remarkable_rails now ships with a new feature, called macro stubs.
80
+ This allows you to declare just once your mocks and/or expectations, and each
81
+ matcher will know how to deal with properly. A TasksController could have your
82
+ specs for a create action rewritten like this:
83
+
84
+ describe TasksController do
85
+ mock_models :task
86
+
87
+ describe :post => :create, :task => { :these => 'params' } do
88
+ expects :new, :on => Task, with => {'these' => 'params'}, :returns => task_proc
89
+ expects :save, :on => mock_task, :returns => true
90
+
91
+ should_assign_to :task, :with => task_proc
92
+ should_redirect_to { task_url(mock_task) }
93
+ end
94
+ end
95
+
96
+ It automatically performs the action before running each macro. In assign_to,
97
+ it executes the expects as expectations (:should_receive), and in redirect_to
98
+ it executes the expects as stubs (:stub!), just as above.
99
+
100
+ For more options, information and configuration, check macro stubs documentation.
101
+
102
+ # v2.x
103
+
104
+ * Added assign_to, filter_params, render_with_layout, respond_with
105
+ respond_with_content_type, route, set_session and set_the_flash matchers.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Carlos Brando
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 ADDED
@@ -0,0 +1,116 @@
1
+ = Remarkable Rails
2
+
3
+ Remarkable Rails is a collection of matchers to Rails. This package has some
4
+ ActionController matchers and soon some ActionView matchers.
5
+
6
+ Whenever using the Remarkable Rails gem, it will automatically add your ActiveRecord
7
+ matchers. So just one line is needed to install both:
8
+
9
+ sudo gem install remarkable_rails
10
+
11
+ If you are using Rails 2.3, you need to have this configuration on your
12
+ config/environments/test.rb:
13
+
14
+ config.gem "rspec", :lib => false
15
+ config.gem "rspec-rails", :lib => false
16
+ config.gem "remarkable_rails", :lib => false
17
+
18
+ And then require remarkable inside your spec_helper.rb, after "spec/rails":
19
+
20
+ require 'spec/rails'
21
+ require 'remarkable_rails'
22
+
23
+ == Matchers & Macros
24
+
25
+ The supported matchers and macros are:
26
+
27
+ assign_to, filter_params, render_with_layout, respond_with,
28
+ respond_with_content_type, route, set_session and set_the_flash matchers.
29
+
30
+ In Remarkable 3.0, we also ported and extended redirect to and render template
31
+ from rspec rails matchers to provide I18n. You can also do:
32
+
33
+ render_template 'edit', :layout => 'default'
34
+ respond_with 404, :content_type => Mime::XML, :body => /Not found/
35
+
36
+ == Macro stubs
37
+
38
+ Another cool feature in Remarkable 3.0 is macro stubs, which makes your mocks
39
+ and stubs DRY and easier to maintain. An rspec default scaffold would be:
40
+
41
+ describe TasksController do
42
+ def mock_task(stubs={})
43
+ @task ||= mock_model(Task, stubs)
44
+ end
45
+
46
+ describe “responding to #POST create” do
47
+ it "exposes a newly created task as @task" do
48
+ Task.should_receive(:new).with({'these' => 'params'}).
49
+ and_return(mock_task(:save => true))
50
+ post :create, :task => {:these => 'params'}
51
+ assigns[:task].should equal(mock_task)
52
+ end
53
+
54
+ it "redirects to the created task" do
55
+ Task.stub!(:new).and_return(mock_task(:save => true))
56
+ post :create, :task => {}
57
+ response.should redirect_to(task_url(mock_task))
58
+ end
59
+ end
60
+ end
61
+
62
+ An equivalent in remarkable would be:
63
+
64
+ describe TasksController do
65
+ mock_models :task
66
+
67
+ describe :post => :create, :task => { :these => 'params' } do
68
+ expects :new, :on => Task, :with => {'these' => 'params'}, :returns => task_proc
69
+ expects :save, :on => task_proc, :returns => true
70
+
71
+ should_assign_to :task, :with => task_proc
72
+ should_redirect_to { task_url(mock_task) }
73
+ end
74
+ end
75
+
76
+ It automatically performs the action before running each macro. It executes the
77
+ expects as expectations (:should_receive), but you can supply :with_stubs => true
78
+ if you want it to be executed with stubs.
79
+
80
+ There are also params and mime methods:
81
+
82
+ describe TasksController
83
+ params :project_id => 42
84
+ mime Mime::HTML
85
+
86
+ describe :get => :show, :id => 37 do
87
+ should_assign_to :project, :task
88
+
89
+ describe Mime::XML do
90
+ should_assign_to :project, :task
91
+ end
92
+ end
93
+ end
94
+
95
+ And much more. Be sure to check macro stubs documentation.
96
+
97
+ == Rails plugin developers
98
+
99
+ Remarkable automatically loads files at the remarkable directory on your plugins
100
+ and frozen gems if RAILS_ROOT is defined. The lookup happens like this:
101
+
102
+ RAILS_ROOT/spec/remarkable
103
+ RAILS_ROOT/vendor/gems/*/remarkable
104
+ RAILS_ROOT/vendor/plugins/*/remarkable
105
+
106
+ Remarkable will load both ruby files (.rb) and Remarkable locale files (.yml).
107
+
108
+ The only step remaining is to include the matchers, which Remarkable will not
109
+ do automatically if the user is not using a Remarkable namespace. For example,
110
+ if the developer includes his matchers to Remarkable::ActiveRecord::Matchers,
111
+ the matchers will be automatically available in users spec. But if he creates
112
+ a new namespace, like MyPlugin::Matchers, he has to tell Remarkable to include
113
+ them in the proper example group:
114
+
115
+ Remarkable.include_matchers!(MyPlugin::Matchers, Spec::Rails::Example::ModelExampleGroup)
116
+
@@ -0,0 +1,73 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{benhutton-remarkable_rails}
8
+ s.version = "4.0.0.alpha4"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Ben Hutton", "Carlos Brando", "José Valim"]
12
+ s.date = %q{2010-11-15}
13
+ s.description = %q{Remarkable Rails: collection of matchers and macros with I18n for Rails}
14
+ s.email = ["benhutton@gmail.com", "eduardobrando@gmail.com", "jose.valim@gmail.com"]
15
+ s.extra_rdoc_files = [
16
+ "CHANGELOG",
17
+ "LICENSE",
18
+ "README"
19
+ ]
20
+ s.files = [
21
+ "CHANGELOG",
22
+ "LICENSE",
23
+ "README",
24
+ "benhutton-remarkable_rails.gemspec",
25
+ "lib/remarkable_rails.rb",
26
+ "lib/remarkable_rails/action_controller.rb",
27
+ "lib/remarkable_rails/action_controller/base.rb",
28
+ "lib/remarkable_rails/action_controller/macro_stubs.rb",
29
+ "lib/remarkable_rails/action_controller/matchers/assign_to_matcher.rb",
30
+ "lib/remarkable_rails/action_controller/matchers/filter_params_matcher.rb",
31
+ "lib/remarkable_rails/action_controller/matchers/redirect_to_matcher.rb",
32
+ "lib/remarkable_rails/action_controller/matchers/render_template_matcher.rb",
33
+ "lib/remarkable_rails/action_controller/matchers/respond_with_matcher.rb",
34
+ "lib/remarkable_rails/action_controller/matchers/route_matcher.rb",
35
+ "lib/remarkable_rails/action_controller/matchers/set_cookies_matcher.rb",
36
+ "lib/remarkable_rails/action_controller/matchers/set_session_matcher.rb",
37
+ "lib/remarkable_rails/action_controller/matchers/set_the_flash_matcher.rb",
38
+ "lib/remarkable_rails/action_view.rb",
39
+ "lib/remarkable_rails/action_view/base.rb",
40
+ "lib/remarkable_rails/active_orm.rb",
41
+ "locale/en.yml",
42
+ "remarkable_rails.gemspec"
43
+ ]
44
+ s.homepage = %q{http://github.com/benhutton/remarkable}
45
+ s.rdoc_options = ["--charset=UTF-8"]
46
+ s.require_paths = ["lib"]
47
+ s.rubyforge_project = %q{remarkable}
48
+ s.rubygems_version = %q{1.3.7}
49
+ s.summary = %q{Remarkable Rails: collection of matchers and macros with I18n for Rails}
50
+
51
+ if s.respond_to? :specification_version then
52
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
53
+ s.specification_version = 3
54
+
55
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
56
+ s.add_runtime_dependency(%q<rspec>, [">= 2.0.0"])
57
+ s.add_runtime_dependency(%q<rspec-rails>, [">= 2.0.0"])
58
+ s.add_runtime_dependency(%q<remarkable>, ["~> 4.0.0.alpha4"])
59
+ s.add_runtime_dependency(%q<remarkable_activerecord>, ["~> 4.0.0.alpha4"])
60
+ else
61
+ s.add_dependency(%q<rspec>, [">= 2.0.0"])
62
+ s.add_dependency(%q<rspec-rails>, [">= 2.0.0"])
63
+ s.add_dependency(%q<remarkable>, ["~> 4.0.0.alpha4"])
64
+ s.add_dependency(%q<remarkable_activerecord>, ["~> 4.0.0.alpha4"])
65
+ end
66
+ else
67
+ s.add_dependency(%q<rspec>, [">= 2.0.0"])
68
+ s.add_dependency(%q<rspec-rails>, [">= 2.0.0"])
69
+ s.add_dependency(%q<remarkable>, ["~> 4.0.0.alpha4"])
70
+ s.add_dependency(%q<remarkable_activerecord>, ["~> 4.0.0.alpha4"])
71
+ end
72
+ end
73
+
@@ -0,0 +1,48 @@
1
+ # Load Remarkable
2
+ require 'remarkable/core'
3
+
4
+ # Load spec/rails
5
+ # require 'rspec/rails'
6
+
7
+ # Load Remarkable Rails base files
8
+ dir = File.dirname(__FILE__)
9
+ require File.join(dir, 'remarkable_rails', 'active_orm')
10
+ require File.join(dir, 'remarkable_rails', 'action_controller')
11
+ require File.join(dir, 'remarkable_rails', 'action_view')
12
+
13
+ # Load locale file
14
+ Remarkable.add_locale File.join(dir, '..', 'locale', 'en.yml')
15
+ # Load plugin files if RAILS_ROOT is defined. It loads files at:
16
+ #
17
+ # RAILS_ROOT/spec/remarkable
18
+ # RAILS_ROOT/vendor/gems/*/remarkable
19
+ # RAILS_ROOT/vendor/plugins/*/remarkable
20
+ #
21
+ # Remarkable will load both ruby files (.rb) and Remarkable locale files (.yml).
22
+ #
23
+ # The only step remaining is to include the matchers, which Remarkable will not
24
+ # do automatically if the user is not using a Remarkable namespace. For example,
25
+ # if the developer includes his matchers to Remarkable::ActiveRecord::Matchers,
26
+ # the matchers will be automatically available in users spec. But if he creates
27
+ # a new namespace, like MyPlugin::Matchers, he has to tell Remarkable to include
28
+ # them in the proper example group:
29
+ #
30
+ # Remarkable.include_matchers!(MyPlugin::Matchers, Spec::Rails::Example::ModelExampleGroup)
31
+ #
32
+ if defined?(Rails.root)
33
+ files = []
34
+ files += Dir.glob(File.join(Rails.root, "spec", "remarkable", "*"))
35
+ files += Dir.glob(File.join(Rails.root, "vendor", "{plugins,gems}", "*", "remarkable", "*"))
36
+ files.each do |file|
37
+ begin
38
+ case File.extname(file)
39
+ when ".rb"
40
+ require file
41
+ when ".yml"
42
+ Remarkable.add_locale file
43
+ end
44
+ rescue Exception => e
45
+ warn "[WARNING] Remarkable could not load file #{file.inspect}. Error: #{e.message}"
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,22 @@
1
+ module Remarkable
2
+ module ActionController
3
+ end
4
+ end
5
+
6
+ dir = File.dirname(__FILE__)
7
+ require File.join(dir, 'action_controller', 'base')
8
+ require File.join(dir, 'action_controller', 'macro_stubs')
9
+
10
+ # Load matchers
11
+ Dir[File.join(dir, 'action_controller', 'matchers', '*.rb')].each do |file|
12
+ require file
13
+ end
14
+
15
+ # Include macro_stubs and matchers in Spec::Rails
16
+ if defined?(RSpec::Rails)
17
+ # RSpec::Rails::ControllerExampleGroup.send :include, Remarkable::ActionController::MacroStubs
18
+
19
+ Remarkable.include_matchers!(Remarkable::ActionController, RSpec::Rails::ControllerExampleGroup)
20
+ Remarkable.include_matchers!(Remarkable::ActionController, RSpec::Rails::RoutingExampleGroup)
21
+ end
22
+
@@ -0,0 +1,31 @@
1
+ module Remarkable
2
+ module ActionController
3
+ class Base < Remarkable::Base
4
+
5
+ before_assert :perform_action_with_macro_stubs
6
+
7
+ optional :with_expectations, :default => true
8
+ optional :with_stubs, :default => true
9
+
10
+ protected
11
+
12
+ # Before assertions, call run_action! to perform the action if it was
13
+ # not performed yet.
14
+ #
15
+ def perform_action_with_macro_stubs #:nodoc:
16
+ @spec.send(:run_action!, run_with_expectations?) if @spec.send(:controller)
17
+ end
18
+
19
+ def run_with_expectations? #:nodoc:
20
+ if @options.key?(:with_stubs)
21
+ !@options[:with_stubs]
22
+ elsif @options.key?(:with_expectations)
23
+ @options[:with_expectations]
24
+ else
25
+ true
26
+ end
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,594 @@
1
+ module RSpec
2
+ module Core
3
+
4
+ # This is a hack that we need to run the alias method chain describe
5
+ class Metadata < Hash
6
+ def first_caller_from_outside_rspec_from_caller(list)
7
+ list.detect {|l| l !~ /(\/lib\/rspec\/core)|remarkable_rails/}
8
+ end
9
+ end
10
+
11
+ # Macro stubs makes stubs and expectations easier, more readable and DRY.
12
+ #
13
+ # == Example
14
+ #
15
+ # Let's jump off to an example:
16
+ #
17
+ # describe ProjectsController do
18
+ # describe :get => :show, :id => 37 do
19
+ # expects :find, :on => Project, :with => '37', :returns => proc { mock_project }
20
+ #
21
+ # should_assign_to :project, :with => proc { mock_project }
22
+ # should_render_template 'show'
23
+ #
24
+ # describe Mime::XML do
25
+ # should_assign_to :project
26
+ # should_respond_with_content_type Mime::XML
27
+ # end
28
+ # end
29
+ # end
30
+ #
31
+ # See how the spec is readable: a ProjectsController responding to get show
32
+ # expects :find on Project which a mock project and then should assign to
33
+ # project and render template 'show'.
34
+ #
35
+ # Each macro before asserting will check if an action was already performed and
36
+ # if not, it runs the expectations and call the action.
37
+ #
38
+ # In other words, should assign to macro is basically doing:
39
+ #
40
+ # it 'should assign to project' do
41
+ # Project.should_receive(:find).with('37').and_return(mock_project)
42
+ # get :show, :id => '37'
43
+ # assigns(:project).should == mock_project
44
+ # end
45
+ #
46
+ # By default, all macros perform expectations. You can change
47
+ # this behavior sending :with_stubs or :with_expectations as options:
48
+ #
49
+ # should_assign_to :project, :with_stubs => true
50
+ # should_render_template 'show', :with_expectations => false
51
+ #
52
+ # This also works in the rspec way:
53
+ #
54
+ # it { should assign_to(:project).with_stubs }
55
+ # it { should render_template('show').with_expectations(false) }
56
+ #
57
+ # == Attention!
58
+ #
59
+ # If you need to check that an array is being sent to a method, you need to
60
+ # give an array inside another array, for example:
61
+ #
62
+ # expects :comment_ids=, :on => Post, :with => [1,2,3]
63
+ #
64
+ # Is the same as:
65
+ #
66
+ # Post.comment_ids = (1, 2, 3)
67
+ #
68
+ # And it won't work. The right way to handle this is:
69
+ #
70
+ # expects :comment_ids=, :on => Post, :with => [[1,2,3]]
71
+ #
72
+ # == mock_models
73
+ #
74
+ # You don't have to play with proc all the time. You can call mock_models which
75
+ # creates two class methods that simply returns a proc and a instance method that
76
+ # do the actual mock.
77
+ #
78
+ # describe ProjectsController do
79
+ # mock_models :project
80
+ #
81
+ # And it creates:
82
+ #
83
+ # def self.project_proc
84
+ # proc { mock_project }
85
+ # end
86
+ #
87
+ # # To be used on index actions
88
+ # def self.projects_proc
89
+ # proc { [mock_project] }
90
+ # end
91
+ #
92
+ # def mock_project(stubs={})
93
+ # @project ||= mock_model(Project, stubs)
94
+ # end
95
+ #
96
+ # Then you can replace those lines:
97
+ #
98
+ # expects :find, :on => Project, :with => '37', :returns => proc { mock_project }
99
+ # should_assign_to :project, :with => proc { mock_project }
100
+ #
101
+ # For:
102
+ #
103
+ # expects :find, :on => Project, :with => '37', :returns => project_proc
104
+ # should_assign_to :project, :with => project_proc
105
+ #
106
+ # = Give me more!
107
+ #
108
+ # If you need to set the example group description, you can also call <tt>get</tt>,
109
+ # <tt>post</tt>, <tt>put</tt> and <tt>delete</tt> methods:
110
+ #
111
+ # describe 'my description' do
112
+ # get :show, :id => 37
113
+ #
114
+ # Things start to get even better when we start to talk about nested resources.
115
+ # After our ProjectsController is created, we want to create a TasksController:
116
+ #
117
+ # describe TasksController do
118
+ # params :project_id => '42' #=> define params for all requests
119
+ #
120
+ # # Those two expectations get inherited in all describe groups below
121
+ # expects :find_by_title, :on => Project, :with => '42', :returns => project_proc
122
+ # expects :tasks, :and_return => Task
123
+ #
124
+ # describe :get => :show, :id => '37' do
125
+ # expects :find, :with => '37', :and_return => task_proc
126
+ #
127
+ # should_assign_to :project, :task
128
+ # should_render_template 'show'
129
+ # end
130
+ # end
131
+ #
132
+ # As you noticed, you can define parameters that will be available to all requests,
133
+ # using the method <tt>params</tt>.
134
+ #
135
+ # Finally if you need to write a spec by hand, you can invoke the action and
136
+ # expectations with run_action!, run_expectations! and run_stubs!. Examples:
137
+ #
138
+ # describe :get => :new do
139
+ # expects :new, :on => Project, :returns => project_proc
140
+ #
141
+ # it "should do something different" do
142
+ # run_action!
143
+ # # do you assertions here
144
+ # end
145
+ # end
146
+ #
147
+ # = Performance!
148
+ #
149
+ # Remarkable comes with a new way to speed up your tests. It performs the
150
+ # action inside a before(:all), so you can do:
151
+ #
152
+ # describe "responding to GET show" do
153
+ # get! :show, :id => 37
154
+ #
155
+ # should_assign_to :task
156
+ # should_render_template :show
157
+ # end
158
+ #
159
+ # Or in the compact way:
160
+ #
161
+ # describe :get! => :show, :id => 37
162
+ #
163
+ # The action will be performed just once before running the macros. If any
164
+ # error happens while performing the action, rspec will output an error
165
+ # but ALL the examples inside the example group (describe) won't be run.
166
+ #
167
+ # By now, the bang methods works only when integrate_views is true and this
168
+ # is when you must see a bigger performance gain.
169
+ #
170
+ # This feature comes with some rspec and rspec rails tweakings. So if you want
171
+ # to do something before the action is performed (stubs something or log
172
+ # someone in session), you have to do it giving a block to the action method:
173
+ #
174
+ # get! :show, :id => 37 do
175
+ # login_as(mock_user)
176
+ # end
177
+ #
178
+ # You can still use the compact way and give the block:
179
+ #
180
+ # describe :get => :show, :id => 37 do
181
+ # get! do
182
+ # login_as(mock_user)
183
+ # end
184
+ # end
185
+ #
186
+ class ExampleGroup
187
+ HTTP_VERBS_METHODS = [:get, :get!, :post, :post!, :put, :put!, :delete, :delete!]
188
+
189
+ class_inheritable_reader :expects_chain, :default_action, :default_mime,
190
+ :default_verb, :default_params, :default_xhr,
191
+ :before_all_block
192
+
193
+ module NewClassMethods
194
+
195
+ # Creates a chain that will be evaluated as stub or expectation. The
196
+ # first parameter is the method expected. You can also specify multiple
197
+ # methods to stub and give a block to calculate the returned value. See
198
+ # examples below.
199
+ #
200
+ # == Options
201
+ #
202
+ # * <tt>:on</tt> - Tell which object will receive the expected method.
203
+ # This option is always required.
204
+ #
205
+ # * <tt>:with</tt> - Tell each parameters will be sent with the expected
206
+ # method. This option is used only in expectations and is optional.
207
+ #
208
+ # * <tt>:returns</tt> - Tell what the expectations should return. Not
209
+ # required.
210
+ #
211
+ # * <tt>:times</tt> - The number of times the object will receive the
212
+ # method. Used only in expectations and when not given, defaults to 1.
213
+ #
214
+ # * <tt>:ordered</tt> - When true specifies that expectations should
215
+ # be received in order.
216
+ #
217
+ # == Example
218
+ #
219
+ # expects :new, :on => Project, :returns => :project_proc, :times => 2
220
+ #
221
+ # expects :new, :find, :on => Project, :returns => :project_proc
222
+ #
223
+ # expects :human_attribute_name, :on => Project, :with => :title do |attr|
224
+ # attr.to_s.humanize
225
+ # end
226
+ #
227
+ def expects(*args, &block)
228
+ options = args.extract_options!
229
+ options.assert_valid_keys(:on, :with, :returns, :times, :ordered)
230
+
231
+ args.each do |arg|
232
+ write_inheritable_array(:expects_chain, [ [ arg, options, block] ])
233
+ end
234
+ end
235
+
236
+ # The mime type of the request. The value given will be called transformed
237
+ # into a string and set in the @request.env['HTTP_ACCEPT'] variable.
238
+ #
239
+ # == Examples
240
+ #
241
+ # mime Mime::XML
242
+ # mime 'application/xml+rss'
243
+ #
244
+ def mime(mime)
245
+ write_inheritable_attribute(:default_mime, mime.to_s)
246
+ end
247
+
248
+ # The params used for the request. Calls are always nested:
249
+ #
250
+ # == Examples
251
+ #
252
+ # describe TasksController do
253
+ # params :project_id => 42
254
+ #
255
+ # describe :get => :show, :id => 37 do
256
+ # # will request with params {:id => 37, :project_id => 42}
257
+ # end
258
+ # end
259
+ #
260
+ def params(params)
261
+ write_inheritable_hash(:default_params, params)
262
+ end
263
+
264
+ # Sets the request to perform a XmlHttpRequest.
265
+ #
266
+ # == Examples
267
+ #
268
+ # describe TasksController do
269
+ # xhr!
270
+ # end
271
+ #
272
+ def xhr!(bool=true)
273
+ write_inheritable_attribute(:default_xhr, bool)
274
+ end
275
+
276
+ [:get, :post, :put, :delete].each do |verb|
277
+ module_eval <<-VERB, __FILE__, __LINE__
278
+ # Declares that we want to do a #{verb} request in the given action
279
+ # and with the given params.
280
+ #
281
+ # == Examples
282
+ #
283
+ # #{verb} :action, :id => 42
284
+ #
285
+ def #{verb}(action, params={})
286
+ params(params)
287
+ write_inheritable_attribute(:default_verb, #{verb.inspect})
288
+ write_inheritable_attribute(:default_action, action)
289
+ end
290
+ VERB
291
+ end
292
+
293
+ [:get!, :post!, :put!, :delete!].each do |verb|
294
+ module_eval <<-VERB, __FILE__, __LINE__
295
+ # Declares that we want to do a #{verb} request in the given action
296
+ # and with the given params, but the action is performed just once
297
+ # in the describe group. In other words, it's performed in a
298
+ # before(:all) filter.
299
+ #
300
+ # == Examples
301
+ #
302
+ # #{verb} :action, :id => 42
303
+ #
304
+ def #{verb}(action=nil, params={}, &block)
305
+ #{verb.to_s.chop}(action, params) if action
306
+ write_inheritable_array(:before_all_block, [block]) if block
307
+ run_callbacks_once!
308
+ end
309
+ VERB
310
+ end
311
+
312
+ # Undefine the method run_callbacks so rspec won't run them in the
313
+ # before and after :each cycle. Then we redefine it as run_callbacks_once,
314
+ # which will be used as an before(:all) and after(:all) filter.
315
+ #
316
+ def run_callbacks_once!(&block) #:nodoc:
317
+ unless instance_methods.any?{|m| m.to_s == 'run_callbacks_once' }
318
+ alias_method :run_callbacks_once, :run_callbacks
319
+ class_eval "def run_callbacks(*args); end"
320
+
321
+ before(:all) do
322
+ setup_mocks_for_rspec
323
+ run_callbacks_once :setup
324
+
325
+ before_all_block.each do |block|
326
+ instance_eval(&block)
327
+ end if before_all_block
328
+
329
+ run_action!
330
+ verify_mocks_for_rspec
331
+ teardown_mocks_for_rspec
332
+ end
333
+
334
+ after(:all) do
335
+ run_callbacks_once :teardown
336
+ end
337
+ end
338
+ end
339
+
340
+ # Overwrites describe to provide quick action description with I18n.
341
+ #
342
+ # You can now do:
343
+ #
344
+ # describe :get => :show, :id => 37
345
+ #
346
+ # Which is the same as:
347
+ #
348
+ # describe 'responding to #GET show' do
349
+ # get :show, :id => 37
350
+ #
351
+ # And do this:
352
+ #
353
+ # describe Mime::XML
354
+ #
355
+ # Which is the same as:
356
+ #
357
+ # describe 'with xml' do
358
+ # mime Mime::XML
359
+ #
360
+ # The string can be localized using I18n. An example yml file is:
361
+ #
362
+ # locale:
363
+ # remarkable:
364
+ # action_controller:
365
+ # responding: "responding to #{{verb}} {{action}}"
366
+ # mime_type: "with {{format}} ({{content_type}})"
367
+ #
368
+ # And load the locale file with:
369
+ #
370
+ # Remarkable.add_locale locale_path
371
+ #
372
+
373
+
374
+ def describe_with_verb_params(*args, &block)
375
+ options = args.first.is_a?(Hash) ? args.first : {}
376
+ verb = (options.keys & HTTP_VERBS_METHODS).first
377
+
378
+ if verb
379
+ action = options.delete(verb)
380
+ verb = verb.to_s
381
+
382
+ description = Remarkable.t 'remarkable.action_controller.responding',
383
+ :default => "responding to \#{{verb}} {{action}}",
384
+ :verb => verb.sub('!', '').upcase, :action => action
385
+
386
+ send_args = [ verb, action, options ]
387
+ elsif args.first.is_a?(Mime::Type)
388
+ mime = args.first
389
+
390
+ description = Remarkable.t 'remarkable.action_controller.mime_type',
391
+ :default => "with #{mime.to_sym}",
392
+ :format => mime.to_sym, :content_type => mime.to_s
393
+
394
+ send_args = [ :mime, mime ]
395
+ else # return if no special type was found
396
+ return describe_without_verb_params(*args, &block)
397
+ end
398
+
399
+ args.shift
400
+ args.unshift(description)
401
+
402
+ # Creates an example group, send the method and eval the given block.
403
+ #
404
+ example_group = describe_without_verb_params(*args) do
405
+ send(*send_args)
406
+ instance_eval(&block)
407
+ end
408
+ end
409
+
410
+
411
+ # Creates mock methods automatically.
412
+ #
413
+ # == Options
414
+ #
415
+ # * <tt>:as</tt> - Used to set the model . For example, if you have
416
+ # Admin::Task model, you have to tell the name of the class to be
417
+ # mocked:
418
+ #
419
+ # mock_models :admin_task, :as => "Admin::Task"
420
+ #
421
+ # * <tt>:class_method</tt> - When set to false, does not create the
422
+ # class method which returns a proc.
423
+ #
424
+ # == Examples
425
+ #
426
+ # Doing this:
427
+ #
428
+ # describe ProjectsController do
429
+ # mock_models :project
430
+ # end
431
+ #
432
+ # Will create one instance and two class mock methods for you:
433
+ #
434
+ # def self.project_proc
435
+ # proc { mock_project }
436
+ # end
437
+ #
438
+ # # To be used on index actions
439
+ # def self.projects_procs
440
+ # proc { [ mock_project ] }
441
+ # end
442
+ #
443
+ # def mock_project(stubs={})
444
+ # @project ||= mock_model(Project, stubs)
445
+ # end
446
+ #
447
+ # If you want to create just the instance method, you can give
448
+ # :class_method => false as option.
449
+ #
450
+ def mock_models(*models)
451
+ options = models.extract_options!
452
+ options = { :class_method => true }.merge(options)
453
+
454
+ models.each do |model|
455
+ model = model.to_s
456
+ klass = options[:as] || model.classify
457
+
458
+ if options[:class_method]
459
+ (class << self; self; end).class_eval <<-METHOD
460
+ def #{model}_proc; proc { mock_#{model} }; end
461
+ def #{model.pluralize}_proc; proc { [ mock_#{model} ] }; end
462
+
463
+ alias :mock_#{model} :#{model}_proc
464
+ alias :mock_#{model.pluralize} :#{model.pluralize}_proc
465
+ METHOD
466
+ end
467
+
468
+ self.class_eval <<-METHOD
469
+ def mock_#{model}(stubs={})
470
+ @#{model} ||= mock_model(#{klass}, stubs)
471
+ end
472
+ METHOD
473
+ end
474
+ end
475
+ alias :mock_model :mock_models
476
+
477
+ end
478
+
479
+ protected
480
+
481
+ # Evaluates the expectation chain as stub or expectations.
482
+ #
483
+ def evaluate_expectation_chain(use_expectations=true) #:nodoc:
484
+ return if self.expects_chain.nil?
485
+
486
+ self.expects_chain.each do |method, options, block|
487
+ object = evaluate_value(options[:on])
488
+ raise ScriptError, "You have to give me :on as an option when calling :expects." if object.nil?
489
+
490
+ if use_expectations
491
+ chain = object.should_receive(method)
492
+
493
+ if options.key?(:with)
494
+ with = evaluate_value(options[:with])
495
+
496
+ chain = if with.is_a?(Array)
497
+ chain.with(*with)
498
+ else
499
+ chain.with(with)
500
+ end
501
+ end
502
+
503
+ times = options[:times] || 1
504
+ chain = chain.exactly(times).times
505
+
506
+ chain = chain.ordered if options[:ordered]
507
+ else
508
+ chain = object.stub!(method)
509
+ end
510
+
511
+ chain = if block
512
+ chain.and_return(&block)
513
+ else
514
+ return_value = evaluate_value(options[:returns])
515
+ chain.and_return(return_value)
516
+ end
517
+ end
518
+ end
519
+
520
+ # Instance method run_stubs! if someone wants to declare additional
521
+ # tests and call the stubs inside of it.
522
+ #
523
+ def run_stubs!
524
+ evaluate_expectation_chain(false)
525
+ end
526
+
527
+ # Instance method run_expectations! if someone wants to declare
528
+ # additional tests and call the stubs inside of it.
529
+ #
530
+ def run_expectations!
531
+ evaluate_expectation_chain(true)
532
+ end
533
+
534
+ # Run the action declared in the describe group, but before runs also
535
+ # the expectations. If an action was already performed, it doesn't run
536
+ # anything at all and returns false.
537
+ #
538
+ # The first parameter is if you want to run expectations or stubs. You
539
+ # can also supply the verb (get, post, put or delete), which action to
540
+ # call, parameters, the mime type and if a xhr should be performed. If
541
+ # any of those parameters are supplied, they override the current
542
+ # definition.
543
+ #
544
+ def run_action!(use_expectations=true, verb=nil, action=nil, params=nil, mime=nil, xhr=nil)
545
+ return false if controller.send(:performed?)
546
+
547
+ evaluate_expectation_chain(use_expectations)
548
+
549
+ mime ||= default_mime
550
+ verb ||= default_verb
551
+ action ||= default_action
552
+ params ||= default_params
553
+ xhr ||= default_xhr
554
+
555
+ parent = self.class.parent
556
+ until mime || verb || action || params || xhr
557
+ mime ||= parent.default_mime
558
+ verb ||= parent.default_verb
559
+ action ||= parent.default_action
560
+ params ||= parent.default_params
561
+ xhr ||= parent.default_xhr
562
+ parent = parent.parent
563
+ end
564
+
565
+ raise ScriptError, "No action was performed or declared." unless verb && action
566
+
567
+ request.env["HTTP_ACCEPT"] ||= mime.to_s if mime
568
+ request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' if xhr
569
+ send(verb, action, params)
570
+ end
571
+
572
+ # Evaluate a given value.
573
+ #
574
+ # This allows procs to be given to the expectation chain and they will
575
+ # be evaluated in the instance binding.
576
+ #
577
+ def evaluate_value(duck) #:nodoc:
578
+ if duck.is_a?(Proc)
579
+ self.instance_eval(&duck)
580
+ elsif duck.is_a?(Array)
581
+ duck.map{|child_duck| evaluate_value(child_duck) }
582
+ else
583
+ duck
584
+ end
585
+ end
586
+
587
+ extend NewClassMethods
588
+ class << self
589
+ alias_method :describe_without_verb_params, :describe
590
+ alias_method :describe, :describe_with_verb_params
591
+ end
592
+ end
593
+ end
594
+ end