Friday, July 18, 2008

RSpec MVC boundaries: why I was getting back template locations instead of html

I finally found the rock in my shoe. The reason why response.body wasn't returning html is due to a mechanism in

the RSpec library (gem install) that mocks the interaction with the Views by default, specifically the file controller_example_group.rb. As I surmised before, there are some matching methods that don't make much sense in certain contexts; in fact, as stated in the documentation in the code itself, 'we encourage you to explore using the isolation mode and revel in its benefits'. The ideal RSpec testing truly isolates MVC components from each other.

To override that behavior and have a more 'integrated' test experience, simply declare integrate_views in the spec controller context. I have actually read this in another blog somewhere where the term 'integration mode' was used casually (and I didn't know what it meant at the time).
# == 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.
Just to reiterate, according to my understanding of the authors' views, to integrate the View elements would be almost defeating the purpose of RSpec's approach to testing. Model, Controller, and View elements should all be tested within each others' spheres.

That being said, here is the code responsible for overriding the 'preferred' behavior:
# Use this to instruct RSpec to render views in your controller examples (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
end
def integrate_views? # :nodoc:
@integrate_views
end
Below, you see where the check is made to see if the View elements are to be included (i.e., unless integrate_views?). You can see in the block that follows that check the code that gets executed if the declaration isn't there.
# === render(options = nil, deprecated_status = nil, &block)
#
# This gets added to the controller's singleton meta class,
# allowing Controller Examples to run in two modes, freely switching
# from context to context.
def render(options=nil, deprecated_status=nil, &block)
unless block_given?
unless integrate_views?
@template.metaclass.class_eval do
define_method :file_exists? do
true
end
define_method :render_file do |*args|
@first_render ||= args[0]
end
end
end
end

if matching_message_expectation_exists(options)
expect_render_mock_proxy.render(options, &block)
@performed_render = true
else
unless matching_stub_exists(options)
super(options, deprecated_status, &block)
end
end
end
Bottom line, the boundaries between the Model, View and Controller elements should really get you to think about what needs to be tested in the spec'ed element... but it's nice to know there are options.

No comments: