module Spec module Rails module Example # Controller Examples live in $RAILS_ROOT/spec/controllers/. # # Controller Examples use Spec::Rails::Example::ControllerExampleGroup, # which supports running specs for Controllers in two modes, which # represent the tension between the more granular testing common in TDD # and the more high level testing built into rails. BDD sits somewhere # in between: we want to a balance between specs that are close enough # to the code to enable quick fault isolation and far enough away from # the code to enable refactoring with minimal changes to the existing # specs. # # == Isolation mode (default) # # No dependencies on views because none are ever rendered. The benefit # of this mode is that can spec the controller completely independent of # the view, allowing that responsibility to be handled later, or by # somebody else. Combined w/ separate view specs, this also provides # better fault isolation. # # == Integration mode # # To run in this mode, include the +integrate_views+ declaration # in your controller context: # # describe ThingController do # integrate_views # ... # # In this mode, controller specs are run in the same way that rails # functional tests run - one set of tests for both the controllers and # the views. The benefit of this approach is that you get wider coverage # from each spec. Experienced rails developers may find this an easier # approach to begin with, however we encourage you to explore using the # isolation mode and revel in its benefits. # # == Expecting Errors # # Rspec on Rails will raise errors that occur in controller actions and # are not rescued or handeled with rescue_from. # class ControllerExampleGroup < FunctionalExampleGroup class << self # Use integrate_views to instruct RSpec to render views in # your controller examples in Integration mode. # # describe ThingController do # integrate_views # ... # # See Spec::Rails::Example::ControllerExampleGroup for more # information about Integration and Isolation modes. def integrate_views(integrate_views = true) @integrate_views = integrate_views end def integrate_views? # :nodoc: @integrate_views end def inherited(klass) # :nodoc: klass.integrate_views(integrate_views?) klass.subject { controller } super end def set_description(*args) # :nodoc: super if described_class && described_class.ancestors.include?(ActionController::Base) controller_klass = if superclass.controller_class.ancestors.include?(ActionController::Base) superclass.controller_class else described_class end tests controller_klass end end # When you don't pass a controller to describe, like this: # # describe ThingsController do # # ... then you must provide a controller_name within the context of # your controller specs: # # describe "ThingController" do # controller_name :thing # ... def controller_name(name) tests "#{name}_controller".camelize.constantize end end before(:each) do # Some Rails apps explicitly disable ActionMailer in environment.rb if defined?(ActionMailer) @deliveries = [] ActionMailer::Base.deliveries = @deliveries end unless @controller.class.ancestors.include?(ActionController::Base) Spec::Expectations.fail_with <<-MESSAGE Controller specs need to know what controller is being specified. You can indicate this by passing the controller to describe(): describe MyController do or by declaring the controller's name describe "a MyController" do controller_name :my #invokes the MyController end MESSAGE end @controller.extend ControllerInstanceMethods @controller.integrate_views! if integrate_views? @controller.session = session end attr_reader :response, :request, :controller def integrate_views? @integrate_views || self.class.integrate_views? end # Bypasses any error rescues defined with rescue_from. Useful # in cases in which you want to specify errors coming out of # actions that might be caught by a rescue_from clause that is # specified separately. # # Note that this will override the effect of rescue_action_in_public def bypass_rescue if ::Rails::VERSION::STRING >= '2.2' def controller.rescue_action(exception) raise exception end else def controller.rescue_action_with_handler(exception) raise exception end end end protected def _assigns_hash_proxy @_assigns_hash_proxy ||= AssignsHashProxy.new(self) {@response.template} end private module TemplateIsolationExtensions def file_exists?(ignore); true; end def render_file(*args) @first_render ||= args[0] unless args[0] =~ /^layouts/ end # Rails 2.2 def _pick_template(*args) @_first_render ||= args[0] unless args[0] =~ /^layouts/ PickedTemplate.new end def __action_exists?(params) controller.respond_to? params[:action] end def __template_exists?(args) self.view_paths.respond_to?(:find_template) ? self.view_paths.find_template(args[0][:file], template_format) : false end def render(*args) if ::Rails::VERSION::STRING >= "2.1" return super unless __action_exists?(params) || __template_exists?(args) end if file = args.last[:file].instance_eval{@template_path} record_render :file => file elsif args.last[:inline] super elsif @_rendered record_render(args[0]) else super end end private def record_render(opts) return unless @_rendered @_rendered[:template] ||= opts[:file] if opts[:file] @_rendered[:partials][opts[:partial]] += 1 if opts[:partial] end # Returned by _pick_template when running controller examples in isolation mode. class PickedTemplate # Do nothing when running controller examples in isolation mode. def render_template(*ignore_args); end # Do nothing when running controller examples in isolation mode. def render_partial(*ignore_args); end end end module ControllerInstanceMethods # :nodoc: include Spec::Rails::Example::RenderObserver # === render(options = nil, extra_options={}, &block) # # This gets added to the controller's singleton meta class, # allowing Controller Examples to run in two modes, freely switching # from example group to example group. def render(options=nil, extra_options={}, &block) unless block_given? unless integrate_views? @template.extend TemplateIsolationExtensions end end if matching_message_expectation_exists(options) render_proxy.render(options, &block) @performed_render = true else if matching_stub_exists(options) @performed_render = true else if ::Rails::VERSION::STRING > '2.1' super(options, extra_options, &block) else super(options, &block) end end end end # Rails 2.3 def default_template(action_name = self.action_name) if integrate_views? super else begin super rescue ActionView::MissingTemplate "#{self.class.name.sub(/Controller$/,'').underscore}/#{action_name}" end end end def response(&block) # NOTE - we're setting @update for the assert_select_spec - kinda weird, huh? @update = block super end def integrate_views! @integrate_views = true end private def integrate_views? @integrate_views end def matching_message_expectation_exists(options) render_proxy.__send__(:__mock_proxy).__send__(:find_matching_expectation, :render, options) end def matching_stub_exists(options) render_proxy.__send__(:__mock_proxy).__send__(:find_matching_method_stub, :render, options) end end Spec::Example::ExampleGroupFactory.register(:controller, self) end end end end