Principles of Effective Testing with Capybara

Development

Reading Time: 6 minutes

Currently the world of integration testing is overwhelmed with complexity. Industry standards require adding more layers of abstraction with tools like Cucumber, SitePrism, and others, and as a result it becomes more difficult and tedious for a developer to write a simple test. But as any other part of the codebase, testing scenarios should be simple and fun. They should be designed for readability and changeability.

In this article, I’m going to talk about how to make integration testing simple and effective with bare-bones Capybara. We will walk through several rules which, in my experience, lead to clarity and simplicity.

Let’s start with an example:

scenario "looking for a hotel" do
  visit "/search"
  should_see "Type a destination"
  fill_in "Country", with: "France"
  fill_in "City", with: "Paris"
  set_date "02/17/2099"
  select "1", from: "Rooms"
  select "2", from: "Adults"
  check "I'm travelling for work"
  click_on "Search"
  should_see "1 result found"
  should_see "Test hotel"
end

That is an awesome testing scenario for a few reasons: Any developer with knowledge of the Capybara API can easily read it; it’s obvious what it tests and what it expects, so it’s easy to add more steps and change it; and it doesn’t need changing when markup changes. It only depends on the business requirements.

Now, this scenario has some distinctive features:

  • There are no selectors.
  • All values are checked globally.
  • It tries to maximize usage of Capybara helpers (it sticks to simple means).
  • It extends the Capybara API with should_see, which is a helper that
    improves readability.
  • It also uses a custom helper set_date, which is not
    scenario-specific but rather widget-specific — a helper for a date
    selector widget.

So how do you make all your tests concise and to-the-point like this one? I’ve come up with several rules that can help keep your tests focused on their job, as well as make them easy to write and change.

Define a Limited Language

It’s a good thing to take a user’s perspective into account when you’re writing tests. It allows you to focus on the end result and see the product as your user sees it. That’s why you need an extra level of abstraction, a DSL that speaks in a user’s terms.

You could use Cucumber, but even with that, it’s easy to write steps like When I click on ".my-form button". In this case, you break the abstraction anyway, and now you have to deal with the mental overhead that Cucumber gives you, with nothing in return.

In my experience, it’s best to go with bare-bones Capybara. Note that Capybara already has this layer of abstraction. Think click_on "Purchase" versus find(...).click. The first one searches for an element by text and will refuse to click if it’s not visible or if it’s not a link/button. It also reads nicely.

Of course that might not be enough, and you probably will need to implement your own custom steps. Just make sure they’re on the same level of abstraction as click_on, fill_in, and so on. They should not accept selectors, only text. For example, here’s my most useful helper:

def should_see(text)
  expect(page).to have_content(text)
end

def should_not_see(text)
  expect(page).to have_no_content(text)
end

Use Universal Steps

By “universal steps,” I mean steps that get used in different tests, as opposed to feature-specific steps that only make sense within a certain scenario. Feature-specific steps should be avoided because they add up to mental overhead that a developer needs to deal with when working with a scenario.

Instead, be explicit. Think about fill_in_order_form versus fill_in "quantity". with: ''; fill_in password with ''. The former hides the complexity, and thus forces a developer to look into an implementation. The latter makes it explicit and easily accessible, so you should always be using it.

Avoid Using Selectors

When you use selectors, you switch your mindset from a user’s perspective to a developer’s, from feature to implementation. That’s the wrong way to do it. Users don’t think in selectors. They don’t care about ids and classes. There’s no value in it, nor they are visible to the user.

What selectors do give you is an extra dependency on the code markup, which is an implementation detail. Now your test is going to fail not only because of an actual bug, but also because markup has changed for whatever reason. And it’s not just that tests become fragile; they become harder to read and comprehend, and therefore harder to maintain.

Compare:

within(".order-form") do
  click_on "input[type=submit]"
end

and

click_on "Purchase"

Note how I just don’t care where exactly on the page the button is located. There’s nothing wrong with looking for elements globally — remember, that’s how a user does it.

Use Text Values

Try to always stick to something that a user can see on the page, like text values. It’s really not as hard as it seems. First of all, Capybara out-of-the-box gives you tools that work this way, methods like “click_on” (link and button values), “fill_in” (labels and placeholders), and so on. But even if you need something custom, it’s actually quite easy to implement.

By the way, this is why I don’t use tools like SitePrism. SitePrism allows you to map entities to selectors on the page and work with those entities instead of selectors directly. However, it’s not solving a problem, it’s just hiding it by adding another layer of abstraction. To my mind, that’s even worse, because it makes the problem implicit.

Sign up for a free Codeship Account

Deal with Ambiguity

As I said earlier, there’s nothing wrong with searching for an element on a page globally. But what if there are several buttons on the page with the same title?

Consider for a little bit that maybe this ambiguity should be addressed in the business area. How do you expect a user to behave when he sees two Purchase buttons? (A rhetorical question, of course.)

There can be repetitive elements on a page (think rows in a table). One solution for this could be introducing new ids or classes to the page in order to be able to target them. But that’s not a good way to do it. A better solution would be to depend on the order and use numerables in your test. Consider something like this:

scenario do
  # ...
  within_table_row(3) do
    click_on "Delete"
  end
  # ...
end

# support/helpers.rb
def within_table_row(position)
  row = find_all("table tr")[position]
  within(row) do
    yield
  end
end

Having a proper semantic markup also helps. For example, if you use the aside HTML tag for the menu block and main for the main content, you can easily implement helpers like within_sidebar { ... } and within_main { ... }.

Stick to Semantic Markup

The method click_on works perfectly for buttons and links, but it won’t click on a span, for example. This is by design. You shouldn’t have spans that behave like links in your code anyway. Use proper markup or ask your HTML developer to do so. If it’s clickable, it should be a button or a link. If it’s a form, it should be a form element.

HTML is semantic for a reason, and browsers know better how to deal with different elements in your DOM when they know what they’re dealing with.

Another example of this kind is the method fill_in, which can handle both labels and placeholders. If your markup lacks this element, you may be tempted to use find("input").set.

But consider for a minute, maybe this is actually a UX problem? If there are no placeholders, nor labels on the page, how is the user to decide what should be put in that field and why? Instead of hacking your way through broken markup, consider changing it by adding a placeholder or a label.

Conclusion

In general, never forget that testing should be simple and fun. I often find myself thinking about how can I make the experience even more enjoyable, and I hope you do, too. When you do achieve a certain level of clarity and simplicity in your testing scenarios, it pays off tremendously.

Subscribe via Email

Over 60,000 people from companies like Netflix, Apple, Spotify and O'Reilly are reading our articles.
Subscribe to receive a weekly newsletter with articles around Continuous Integration, Docker, and software development best practices.



We promise that we won't spam you. You can unsubscribe any time.

Join the Discussion

Leave us some comments on what you think about this topic or if you like to add something.