Skip to content

Instantly share code, notes, and snippets.

@jmondo
Created June 27, 2012 03:42
Show Gist options
  • Save jmondo/3001238 to your computer and use it in GitHub Desktop.
Save jmondo/3001238 to your computer and use it in GitHub Desktop.
Asserting position blog post draft 1
layout title date comments categories keywords
post
Rspec: Asserting Positions The Right Way
2012-06-26 18:25
true
rails
Rspec
integration
Rspec
integration testing
position
assertions

At LaunchWare, we've been looking for the best way to locate an element on a page in an Rspec request spec. Sometimes there's just no getting around asserting that one piece of content appears before another, like when testing sort options. We've tried a few things:

The exposition

Imagine there are two comments on a post. We want to assert that the most recent comment appears first.

We have the following markup:

<div>
  <div class="comment">
    <p>Here is a newer comment!</p>
  </div>
  <div class="comment">
    <p>Here is a comment!</p>
  </div>
</div>

The most obvious way

The most obvious way to assert that the newer commment appears first is:

let!(:old_comment) { Factory(:comment) }
let!(:new_comment) { Factory(:comment) }

within "div.comment:nth-child(1)" do
  page.should have_content new_comment.text
end

within "div.comment:nth-child(2)" do
  page.should have_content old_comment.text
end

Or some variant of this. page.find("div.comment:nth-child(1)") works too.

But this approach is brittle. What if we go all HTML5 on this template and change the comment divs to article tags like so:

<article>
  <article class="comment">
    <p>Here is a newer comment!</p>
  </article>
  <article class="comment">
    <p>Here is a comment!</p>
  </article>
</article>

Then our test is broken.

Or worse, if we add another comment into the mix as part of another test:

let!(:old_comment) { Factory(:comment) }
let!(:new_comment) { Factory(:comment) }
let!(:some_other_comment) { Factory(:comment) }

Our markup becomes:

<article>
  <article class="comment">
    <p>Here is a comment!</p>
  </article>
  <article class="comment">
    <p>Here is a newer comment!</p>
  </article>
  <article class="comment">
    <p>Here is a comment!</p>
  </article>
</article>

Not nth-child 1 and 2 anymore...

But hey, we don't really care which children they are, or what tags they are. We just want to know if the text of the new comment is on the page and placed somewhere before the old comment! Let's test that. So, here you have it:

The less brittle (and therefore better) way

We know thse things:

  1. With capybara, you can grab the contents of the whole page as a string by calling page.body.
  2. And with a Ruby string, you can grab the position (index) of a certain substring by calling index(substring).
  3. And with Rspec you can assert that one index/position is less than/before another using index1.should_be < index2.

So put that all together, and we replace the brittle approach above with this:

let!(:old_comment) { Factory(:comment) }
let!(:new_comment) { Factory(:comment) }

page.body.index(new_comment.text).should < page.body.index(old_comment.text)

Awesome!

But there's one more thing:

The one more thing (best) way

It would be annoying to type that over and over. What if you or the guys at LaunchWare figure out a better way to do this in the future? You wouldn't want to have to find-and-replace your whole code base. And we wouldn't either.

Try this Rspec custom matcher definition on for show:

Rspec::Matchers.define :appear_before do |later_content|
  match do |earlier_content|
    page.body.index(earlier_content) < page.body.index(later_content)
  end
end

Once this is in play, you can now make this magical assertion:

let!(:old_comment) { Factory(:comment) }
let!(:new_comment) { Factory(:comment) }

new_comment.text.should appear_before(old_comment.text)

Definitely try this at home:

How to add this to your project

All you have to do is create a module with the Rspec matcher and hook it up to your spec_helper.rb file.

The module:

# /spec/support/position_helper.rb
module PositionHelper
  Rspec::Matchers.define :appear_before do |later_content|
    match do |earlier_content|
      page.body.index(earlier_content) < page.body.index(later_content)
    end
  end
end

Add the module to your Rspec configuration:

# end of /spec/spec_helper.rb
  # ...
  config.include(PositionHelper)
end

And you're in business.

#win

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment