Skip to content
This repository

rspec-rails + rabl rendering views even if I don't want them to be rendered? #565

Closed
agibralter opened this Issue June 20, 2012 · 13 comments

3 participants

Aaron Gibralter David Chelimsky David Sommers
Aaron Gibralter

This is verbatim of my stackoverflow question, but I did some debugging and think that it's not an issue with my setup.

I'm using rabl 0.6.13, rspec-rails 2.10.1, and rails 3.2.6.

I'm trying to spec my controllers in isolation, but for some reason my rabl templates are throwing me all sorts of undefined method exceptions on the mocks I use in my controller specs. I am not using render_views. I thought rspec did not process views unless you specify render_views in the controller spec. I've run debugger to ensure that render_views? evaluates to false in the before block rspec-rails inserts. Has anyone else run into this problem?

Basically I put a debugger in Haml::Plugin.call and saw that it was only breaking on that statement if my rspec context included render_views (using the param format => :html). Then I noticed that if I put that same debugger statement ActionView::Template::Handlers::Rabl.call the spec would break there regardless of whether or not I included render_views in the rspec context (using the param format => :json). Does that make sense? At first I thought it was rabl... but now it seems like something is going on in rails or rspec-rails that is causing rendering when the format is json or when the handler is rabl or some combination of both. Is that intended behavior?

David Chelimsky
Owner
Aaron Gibralter

Hmm yeah I was poking through there, but I'm still having trouble getting to the bottom of this. It seems that when passed either format and render_views? in rspec is false, execution gets to ActionView::Template.render (here)[https://github.com/rails/rails/blob/v3.2.6/actionpack/lib/action_view/template.rb#L145]:

def render(view, locals, buffer=nil, &block)
  ActiveSupport::Notifications.instrument("!render_template.action_view", :virtual_path => @virtual_path) do
    compile!(view)
    view.send(method_name, locals, buffer, &block)
  end
rescue Exception => e
  handle_render_error(view, e)
end

Both with haml and rabl, the compile! happens and (this)[https://github.com/rails/rails/blob/v3.2.6/actionpack/lib/action_view/template.rb#L268] sets the local variable code to be either haml-y or rabl-y:

def compile(view, mod) #:nodoc:
  encode!
  method_name = self.method_name
  code = @handler.call(self)

  # Make sure that the resulting String to be evalled is in the
  # encoding of the code
  source = <<-end_src
    def #{method_name}(local_assigns, output_buffer)
      _old_virtual_path, @virtual_path = @virtual_path, #{@virtual_path.inspect};_old_output_buffer = @output_buffer;#{locals_code};#{code}
    ensure
      @virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer
    end
  end_src

  # ...

end

But in the end, after the compile! is done, view.send(method_name, locals, buffer, &block) evaluates to "" for when html is the format and haml is the handler, and view.send(method_name, locals, buffer, &block) raises an exception of undefined method error because a mock gets touched in my rabl template.

And strangely, when I step into the view.send method (again, with render_views false) I find myself in either index.json.rabl or index.html.haml.

Aaron Gibralter

Ah, it seems like there is an issue open on rabl about this: nesquena/rabl#37

I'm still wondering though if it's possible to cut of template rendering at a higher level than is currently implemented in rspec-rails. Even with the stubbing of blank templates, a lot of the underlying template handler (haml, rabl, etc.) gets called without render_views on.

David Chelimsky
Owner

We need all that to be able to spec that the correct template is rendered when you have multiple handlers, mime types, etc.

Aaron Gibralter

Ok, so I took another look. What do you think about this:

Basically rspec-rails creates a new ActionView::Template passing "" as the source for the initializer. Here's the thing though, the initializer sets @source = "" in ActionView::Template#initialize, but the compile method pretty much ignores @source:

https://github.com/rails/rails/blob/master/actionpack/lib/action_view/template.rb#L264-305

def compile(view, mod) #:nodoc:
  encode!
  method_name = self.method_name
  code = @handler.call(self)

  # Make sure that the resulting String to be evalled is in the
  # encoding of the code
  source = <<-end_src
    def #{method_name}(local_assigns, output_buffer)
      _old_virtual_path, @virtual_path = @virtual_path, #{@virtual_path.inspect};_old_output_buffer = @output_buffer;#{locals_code};#{code}
    ensure
      @virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer
    end
  end_src

  # ...

  begin
    mod.module_eval(source, identifier, 0)
    ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
  rescue Exception => e # errors from template code
    # ...
  end
end

Would it make sense at all to create a handler decorator in rspec-rails that stubs out the call method?

class EmptyTemplateHandler
  def self.call(template)
    ""
  end
end

::ActionView::Template.new(
  "",
  template.identifier,
  EmptyTemplateHandler, # instead of template.handler,
  {
    :virtual_path => template.virtual_path,
    :format => template.formats
  }
)
Aaron Gibralter

Oh, and here is the problem with rabl: https://github.com/nesquena/rabl/blob/master/lib/rabl/template.rb#L53-57

def self.call(template)
  source = if template.source.empty?
    File.read(template.identifier)
  else # use source
    template.source
  end

  %{ ::Rabl::Engine.new(#{source.inspect}).
      render(self, assigns.merge(local_assigns)) }
end # call

If source is empty, it re-reads it from the filesystem!

I can't tell if that's a code smell on Rabl's part, or if rspec-rails should perhaps accomodate template engines that get upset with an empty source?

Perhaps the source could be " " instead of ""?

David Sommers

I believe you're right on the file read. That shouldn't be there. It came from this commit but included no explanation besides a reference to an issue that the author of the patch had. It looks like a hack. I'll push to master a simple source = template.source and you can try it from there.

I'll continue the conversation here: nesquena/rabl#37

David Sommers

Resolved in Rabl. Thanks again to both of you for spending the time to look into it. And thanks to @dchelimsky for a great testing framework.

Aaron Gibralter

Thank you, @databyte! I think that solves it for now. And sorry for the trouble, @dchelimsky. I would, however, like to hear your thoughts on stubbing out the call method -- it could be overkill though...

Aaron Gibralter agibralter closed this June 20, 2012
David Sommers

I think it works since the call method is defined on the handlers. I saw you mentioned HAML but looking at their template call, it looks like rspec works fine there as well.

Aaron Gibralter

Yeah it seems that rspec-rails works in the vast majority of cases (this rabl issue was the only one I've run into). But if the result of the call is going to be basically nothing anyway (just skeleton compiled haml code and rabl code), why not just stub out that call code and maybe speed up controller specs that are running in isolation? Eh, maybe there are some template languages where the call code has significant side effects for the controller... in which case stubbing that out would break specs.

David Chelimsky
Owner

@agibralter, @databyte thank you both for digging in and resolving this. Personally, I think an empty template is a perfectly legitimate scenario and ought not be disallowed by any template engine.

I also agree stubbing call would risk covering up behavior that a templating engine might need to do its job. I think we should leave things as/is.

Aaron Gibralter

Cool, cool -- I just wanted to check and see what you though about that. I agree too. Thanks again for the help!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.