We’ve got a few mantras at Pivotal. One of them has to do with testing all the time. It’s a Good Thing, for sure. Until recently though, I had always inserted a tacit “except for views” to the end of it. The reason for my reservations wasn’t the fact that view tests can be brittle. Any test can be brittle. I didn’t like testing views because it seemed like the test I was writing never really described the code I was writing. Let’s look at a typical view test to see what I mean:
describe "/posts/index.html.erb" do
def render_view
render "/posts/index.html.erb"
response.body
end
before(:each) do
assigns[:posts] = [
stub_model(Post, :name => "First!", :body => "first body."),
stub_model(Post, :name => "Second!", :body => "second body.")
]
end
describe "assertions using have_tag" do
it "renders posts" do
render_view
response.should have_tag(".post", 2)
end
it "renders post headers" do
render_view
response.should have_tag(".post .post-name", "First!", 1)
response.should have_tag(".post .post-name", "Second!", 1)
end
it "renders post bodies" do
render_view
response.should have_tag(".post .post-body", "first body.", 1)
response.should have_tag(".post .post-body", "second body.", 1)
end
end
end
This snippet uses the have_tag
helper. It’s somewhat slow, and to my eyes, expresses intent about as well as an apple floating in a top-hat filled with perfume. The “tag” is a selector? The content filter is just the second argument? And the last argument is the amount of “tag” the response should have? You can test like this, but why would you?
I’ve also seen a more manual pattern, using a library like Hpricot or Nokogiri to parse the response body, then asserting on the results of that:
describe "assertions using Nokogiri" do
def doc
@doc ||= Nokogiri(render_view)
end
it "renders posts" do
doc.search('.post').should have(2).nodes
end
it "renders post headers" do
headers = doc.search('.post .post-name')
headers.should have(2).elements
headers.detect { |element| element.text == "First!" }.should_not be_nil
headers.detect { |element| element.text == "Second!" }.should_not be_nil
end
it "renders post bodies" do
bodies = doc.search('.post .post-name')
bodies.should have(2).elements
bodies.detect { |element| element.text == "first body." }.should_not be_nil
bodies.detect { |element| element.text == "second body." }.should_not be_nil
end
end
It’s faster, since it’s not using have_tag
, but still not very expressive. CSS selectors are still littered across the it
statements, but at least it’s only once per test. Still, using detect
to find content is no good. And I don’t think CSS selectors have any business in it
statements at all. That seems like asserting on the name of a method being called, not its behavior.
The solution!
Given my problems with the above approaches, I created a gem that allows the following assertion syntax:
it "renders posts" do
result.should have(2).posts
end
it "renders post headers" do
result.should have(2).post_headers
result.should have(1).post_header.with_text("First!")
result.should have(1).post_header.with_text("Second!")
end
it "renders post bodies" do
result.should have(2).post_bodies
result.should have(1).post_body.with_text("first body.")
result.should have(1).post_body.with_text("second body.")
end
What’s a result
? And how does it know how many posts
, post_headers
, and post_bodies
it has? The result is defined in a before
block like so:
require 'elementor'
require 'elementor/spec'
include Elementor
attr_reader :result
before(:each) do
@result = elements(:from => :render_view) do |tag|
tag.posts ".post"
tag.post_headers ".post .post-name"
tag.post_bodies ".post .post-body"
end
end
The elements
method allows you to name your CSS selectors using the tag
block argument. The tag
object uses method_missing
to register your names. The :from
option specifies a method to be called that will return some raw markup.
Naming selectors alone was a huge win for me, but there are a few other cool bits about the @result
object. First, you get to use the with_text
helper for filtering content. You’ll also get a with_attrs
helper for filtering based on a hash of attribute values.
The project is called Elementor, and you can install it like so:
[sudo] gem install elementor
The code is on the GitHub here: github.com/nakajima/elementor (and you can see the CI build here). Take a look at the specs for all of the examples of what you can do. Hopefully, you’ll find it as useful as I have. If not, please share your reasons in the comments!
About the Author