Blog

Nov

Simple tricks to clean up your Capybara tests


I'd like to share with you a few simple tricks we use all the time at Elabs to clean up our Capybara tests.

Often we want to find a specific area of the page, and perform some action in that area. With the imminent release of Capybara 2.0, you will probably find yourselves doing this even more, since you will sometimes have to be more specific about which element you want to interact with, since Capybara will raise an error when more than one element can be found.

You are probably familiar with the within method in Capybara, so say you want to click a link in the main menu, you might do it like this:

within "header nav" do
  click_link "Archive"
end

But in Capybara, all methods like click_link and fill_in and so on can be chained, so you could have written the above more succinctly as:

find("header nav").click_link("Archive")

That's better. However, the central idea behind Capybara is that you should test your application the way a regular user would use it; your user knows what the main menu is, but they don't really know anything about that CSS selector.

I once wrote a blog post called "You're cuking it wrong", about best practices when using Cucumber, it ended with the following general rule:

A step description should never contain regexen, CSS or XPath selectors, any kind of code or data structure. It should be easily understood just by reading the description.

While it doesn't make sense to be as strict about this ideal in plain Capybara tests, there is still a point to be made about avoiding low level detail even in these tests.

With that in mind, let's revisit that example. What if we could simply write:

main_menu.click_link("Archive")

That's much more obvious. We clearly communicate the intent, and we don't include any irrelevant details in the test. The implementation is super simple:

module MenuSteps
  def main_menu
    find("header nav")
  end
end

Now you might ask, what if I want to perform a lot of actions in a particular area of the page, this doesn't look so nice:

login_form.fill_in "Email", :with => "jonas@elabs.se"
login_form.fill_in "Password", :with => "capybara"
login_form.click_button "Login"

Thankfully, the within method can actually take as an argument an actual element, not just a selector. So we can write this instead:

within login_form do
  fill_in "Email", :with => "jonas@elabs.se"
  fill_in "Password", :with => "capybara"
  click_button "Login"
end

Concise and clear.

Arguments

Of course, since main_menu and login_form are just methods, we could create similar methods which take arguments. Imagine we wanted to delete a particular comment on our blog. We could write this:

find(".comment", :text => "Worst article ever!").click_on("Remove")

Hopefully you can see where this is going:

comment("Worst article ever").click_on("Remove")

And the implementation:

module CommentSteps
  def comment(text)
    find(".comment", :text => text)
  end
end

Much better!

Assertions

This is a bit specific to RSpec, but if you're using a different test framework, you can adapt this to your needs.

In another test for our blog, we are creating comments, and we want to assert that the comment appears as intended.

page.should have_selector(".comment", :text => "Best article ever!")

Remember our guideline/rule. That selector has to go! It would be fantastic if we could simply write:

page.should have_comment("Best article ever!")

But now we have a problem. We could create an RSpec matcher, but that would actually be a bit cumbersome. It could look like this:

module CommentSteps
  extend RSpec::Matchers::DSL

  matcher :have_comment do |text|
    match_for_should { |node| node.has_selector?(".comment", :text => text) }
    match_for_should_not { |node| node.has_no_selector?(".comment", :text => text) }
  end
end

It's important that you add both match_for_should and match_for_should_not, otherwise Capybara's waiting behaviour doesn't work properly.

This also doesn't give us particularly great error messages. We will get some variation on "expected true to be false", which isn't particular helpful.

There is a much easier way, which works well for simple cases like these:

module CommentSteps
  def have_comment(text)
    have_selector(".comment", :text => text)
  end
end

Same functionality, but easier to understand, shorter, and it even gives us better errors. We are making use of the fact that have_selector just returns an RSpec matcher.

Ensure on

One thing we face often in our tests is that we need to be on a particular page to do something. This is especially important if we have abstracted something into a helper. Consider this:

module SessionSteps
  def login
    visit login_path
    fill_in "Email", :with => "jonas@elabs.se"
    fill_in "Password", :with => "capybara"
    click_button "Login"
  end
end

But what if we already are on the login page when we call login, we'll visit the page again, which wastes valuable execution time. Let's make sure we don't do that:

module SessionSteps
  def login
    visit login_path unless current_path == login_path
    fill_in "Email", :with => "jonas@elabs.se"
    fill_in "Password", :with => "capybara"
    click_button "Login"
  end
end

We immediately spot that this pattern would be sensible to abstract. We have this in almost all of our projects, and we call it ensure_on:

module CommonSteps
  def ensure_on(path)
    visit(path) unless current_path == path
  end
end

Usage should be fairly obvious:

module SessionSteps
  def login
    ensure_on login_path
    fill_in "Email", :with => "jonas@elabs.se"
    fill_in "Password", :with => "capybara"
    click_button "Login"
  end
end

And for our final trick…

Suppose that on our blog, comments are moderated, and when someone posts a comment, we want to check that it appears in the moderation queue:

fill_in("Comment", :with => "Hi there")
click_on("Post")
visit moderation_queue_path
find(".moderation-queue").should have_comment("Hi there")

Of course we should create a moderation_queue method, like we did in the first example. But then we still need to remember that we need to actually visit that page before we fetch the element. Let's abstract that away:

module ModerationQueueSteps
  def moderation_queue
    ensure_on moderation_queue_path
    find(".moderation-queue")
  end
end

Now we can write:

fill_in("Comment", :with => "Hi there")
click_on("Post")
moderation_queue.should have_comment("Hi there")

That reveals our intent much clearer. We have practically created a small little DSL for our tests, just by creating a couple of really simple helper methods.

So that's it, some really simple tricks which allow you to write cleaner, more understandable tests with Capybara.

blog comments powered by Disqus