14

I'm running Rails 3.1.1, RSpec 2.7.0 and HAML 3.1.3.

Say I have the following view files:

app/views/layouts/application.html.haml
!!!
%html
  %head
    %title Test
    = stylesheet_link_tag "application"
    = javascript_include_tag "application"
    = csrf_meta_tags

  %body
    = content_for?(:content) ? yield(:content) : yield
app/views/layouts/companies.html.haml
- content_for :content do
  #main
    = yield :main
  #sidebar
    = yield :sidebar

= render :template => 'layouts/application'
app/views/companies/index.html.haml
- content_for :main do
  %h1 MainHeader
- content_for :sidebar do
  %h1 SidebarHeader

And the following spec file:

spec/views/companies/index_spec.rb
require 'spec_helper'

describe 'companies/index.html.haml' do

  it 'should show the headers' do
    render
    rendered.should contain('MainHeader')
    rendered.should contain('SidebarHeader')
  end

end

When I run RSpec, I get the following error:

1) companies/index.html.haml should show the headers
   Failure/Error: rendered.should contain('MainHeader')
     expected the following element's content to include "MainHeader":
   # ./spec/views/companies/index_spec.rb:7:in `block (2 levels) in <top (required)>'

At first, I thought RSpec was somehow missing the content_for blocks when rendering the view files. However, I was not able to find any issue related to it on RSpec's github repository, so I'm not sure who's to blame here.

One (recent) solution I found is at http://www.dixis.com/?p=571. However, when I try the suggested code

view.instance_variable_get(:@_content_for)

it returns nil.

  • Is there a way to test content_for in view specs?
  • Is there a better way to structure my layout files, such that I'm actually able to test them and still achieve the same end result?
Tomas Mattia
  • 371
  • 2
  • 7

3 Answers3

27

Using Rspec 2 with Rails 3, in order to write view specs for usage of content_for, do this:

view.content_for(:main).should contain('MainHeader')
# instead of contain() I'd recommend using have_tag (webrat)
# or have_selector (capybara)

p.s. the value of a content_for(...) block by default is an empty string, so if you want to write specs showing cases in which content_for(:main) does not get called, use:

view.content_for(:main).should be_blank

Your spec could be written as:

it "should show the headers" do
  render
  view.content_for(:main).should contain('MainHeader')
  view.content_for(:side_header).should contain('SidebarHeader')
end

This way your spec shows exactly what your view does, independent of any layout. For a view spec, I think it's appropriate to test it in isolation. Is it always useful to write view specs? That's an open question.

Instead if you want to write specs showing what the markup served to the user looks like, then you'll want either a request spec or a cucumber feature. A third option would be a controller spec that includes views.

p.s. if you needed to spec a view that outputs some markup directly and delegates other markup to content_for(), you could do that this way:

it "should output 'foo' directly, not as a content_for(:other) block" do
   render
   rendered.should contain('foo')
   view.content_for(:other).should_not contain('foo')
end
it "should pass 'bar' to content_for(:other), and not output 'bar' directly" do
   render
   rendered.should_not contain('bar')
   view.content_for(:other).should contain('bar')
end

That would probably be redundant, but I just wanted to show how render() populates rendered and view.content_for. "rendered" contains whatever output the view produces directly. "view.content_for()" looks up whatever content the view delegated via content_for().

Benissimo
  • 1,010
  • 9
  • 14
  • 1
    I found this quite useful. However, I am using Rspec 2.8, and using `contain` would raise the error `undefined method 'contain' for #`. But I simply use `==` instead as in `view.content_for(:other).should == 'bar'` and it works great. – evanrmurphy Mar 14 '12 at 15:34
  • 1
    Thanks for the positive feedback! If you want to test for a substring and "contain" isn't supported for your copy of rspec, you could use "include": view.content_for(:other).should include('bar'). – Benissimo Mar 27 '12 at 08:19
1

From the RSpec docs:

To provide a layout for the render, you'll need to specify both the template and the layout explicitly.

I updated the spec and it passed:

require 'spec_helper'

describe 'companies/index.html.haml' do

  it 'should show the headers' do
    render :template => 'companies/index', :layout => 'layouts/companies'
    rendered.should contain('MainHeader')
    rendered.should contain('SidebarHeader')
  end

end
Tomas Mattia
  • 371
  • 2
  • 7
-7

Do not bother with view specs. They're hard to write well, and they don't test enough of the stack to be worth using (especially in view of the difficulty writing). Instead, use Cucumber, and test your views in the course of that.

You generally don't want to test content_for specifically either: that's implementation, and you should instead be testing behavior. So just write your Cucumber stories so they test for the desired content.

If for some odd reason you do need to test content_for, RSpec has a syntax that's something like body[:content_name] or body.capture :content_name depending on the version (or something like that; haven't used it in a while). But consider carefully whether there's a better way to test what you actually want to test.

Marnen Laibow-Koser
  • 5,959
  • 1
  • 28
  • 33
  • IMHO, a Decorator or Presenter can be used to make it OK not to write view specs, and can also DRY up your views or even make them logicless. But we shouldn't simply stop at saying "do not bother with view specs" -- that's being a little too general. – Robert Brandin Feb 22 '12 at 20:34
  • 1
    @MarnenLaibow-Koser performance perhaps. I wrote request specs to test the meta tags on every one of my application's pages, but they're so slow I find myself reluctant to run them. Currently translating them to view specs, and initial results show that they're faster. – evanrmurphy Mar 13 '12 at 16:52
  • @evanrmurphy They may be faster, but they don't test what you actually need tested. (Neither do request specs, really -- Cucumber is better in this respect -- but they're less wrong than view specs.) Especially in your test code, worry about correctness first, performance second. – Marnen Laibow-Koser Mar 14 '12 at 16:59
  • To amplify my last sentence: an incorrect test is arguably worse than none at all, because it gives you a false sense of assurance that your application is correct. – Marnen Laibow-Koser Mar 14 '12 at 20:46
  • BTW, a major reason that view specs are wrong and useless is that they test a particular view file, but neither they nor anything else guarantees that that view file is actually being rendered by the controller. – Marnen Laibow-Koser May 16 '12 at 16:03
  • @RobertBrandin It isn't too general. There is simply nothing at all that view specs are good for in Rails, ever, under any circumstances. Testing a view is difficult and useless, since nothing ever tests that that view is being rendered in the right place. Cucumber is the right tool for this job. If you think you have a use case for view specs despite this, I'd be very interested to hear about a concrete example. – Marnen Laibow-Koser Dec 11 '13 at 03:36
  • I find the statement "do not bother" too broad, I wanted to test the link of a cancel button and don't want to invoke the entire stack for that. – Jon Lauridsen Aug 03 '16 at 10:02
  • @JonLauridsen While that may sometimes seem tempting, you really _have_ to invoke the entire stack in order to be sure that the view is rendering where you think it is. In other words, testing the view alone cannot test that the view actually appears in the part of the app where you think it does. You might have, for example, a view that's correct in isolation but never actually displayed to the user when it should be (I have actually had things like that happen). – Marnen Laibow-Koser Jan 18 '17 at 21:09
  • I see your point, but the scenario you describe I'd cover with an end-to-end test that verifies the button exists, probably as part of a flow that interacts with elements to achieve some business goal. But what I wanted to test is a link_to ends up correct, which I think should be isolated so I can easily test its boundaries. We don't have to agree, but perhaps you can see my point of view. – Jon Lauridsen Jan 20 '17 at 01:22
  • @JonLauridsen If you have the end-to-end test, just that the link there. The view test adds nothing useful at all. – Marnen Laibow-Koser Jan 24 '17 at 14:40
  • Let's agree to disagree. My link_to had some presentation-logic associated that I wanted to test thoroughly, for that I didn't care if it's ever really rendered. In fact I want to test it in isolation, that way my end-to-end tests only focus on basic customer-oriented happy-paths. But we don't have to agree. Thank you for a good internet discussion. – Jon Lauridsen Jan 25 '17 at 23:57
  • @JonLauridsen Agreeing to disagree has no place in a field like software engineering, which ought to be based on facts, logic, and evidence, not opinion. If we disagree, most likely _at least_ one of us is wrong, and it would be to both our benefits to figure out which one (or both). – Marnen Laibow-Koser Jan 26 '17 at 01:26
  • @JonLauridsen "In fact I want to test it in isolation, that way my end-to-end tests only focus on basic customer-oriented happy-paths." Surely if it's worth testing, it adds customer value. If that is so, it is normally worth putting into the end-to-end tests. As you describe it now (if I understand correctly!), you have a component of your application that you _believe_ is well tested but is not. Also, it sounds like you may underestimate what end-to-end tests are good for. Why limit them to only a few happy paths when they could do so much more for you? – Marnen Laibow-Koser Jan 26 '17 at 01:30
  • Well if we work together we can argue, but this is an internet discussion, there are realistic limits :) I prefer happy-path focused e2e tests that sanity-check my application, and unit tests for testing all boundaries. In this case the cancel button's link_to value had some boundaries that I wouldn't want an e2e test to exercise, it'd just be too heavy. I forget the exact details though as I've moved on from that company. – Jon Lauridsen Jan 27 '17 at 14:15
  • @JonLauridsen What would you consider boundaries that you wouldn't want your end-to-end tests to exercise? That's precisely what they're for, no? (It looks like you have a relatively limited view of the utility of end-to-end tests...) – Marnen Laibow-Koser Jan 27 '17 at 20:50