Note: This article has been cross posted on the UrbanBound product blog.
A good suite of reliable feature/acceptance tests is a very valuable thing to have. It can also be incredibly difficult to create. Test suites that are driven by tools like Selenium or Poltergeist are usually known for being slow and flaky. And, flaky/erratic tests can cause a team to lose confidence in their test suite, and question the value of the specs as a whole. However, much of this slowness and flakiness is due to test authors not making use of the proper Capybara APIs in their tests, or by overusing calls to sleep to get around race conditions.
The Common Problem
Take the following code for example, which clicks a link with the id
"foo", and checks to make sure that the message
"Loaded successfully" displays in the proper spot on the page.
There are a few potential problems here. Let’s talk about them below.
Capybara’s Finders, Matchers, and Actions
Capybara provides several tools for working with asynchronous requests.
Capybara provides a number of finder methods that can be used to find elements on a page. These finder methods will wait up to the amount of time specified in
Capybara.default_wait_time (defaults to 2 seconds) for the element to appear on the page before raising an error that the element could not be found. This functionality provides a buffer, giving time for the AJAX request to complete and for the response to be processed before proceeding with the test, and helps eliminate race conditions if used properly. It will also only wait the amount of time it needs to, proceeding with the test as soon as the element has been found.
In the example above, it should be noted that Capybara’s
first API will not wait for
.message to appear on the DOM. So if it isn’t already there, the test will fail. Using
find addresses this issue.
The test will now wait for an element with the class
.message to appear on the page before checking to see if it contains
"Loaded successfully". But, what if
.message already exists on the page? It is still possible that this test will fail because it is not giving enough time for the value of
.message to be updated. This is where the matchers come in.
Capybara provides a series of Test::Unit / Minitest matchers, along with a corresponding set of RSpec matchers, to simplify writing test assertions. However, these matchers are more than syntactical sugar. They have built in wait functionality. For example, if
has_text does not find the specified text on the page, it will wait up to
Capybara.default_wait_time for it to appear before failing the test. This makes them incredibly useful for testing asynchronous behavior. Using matchers will dramatically cut back on the number of race conditions you will have to deal with.
Looking at the example above, we can see that the test is simply checking to see if the value of the element with the class
"Loaded successfully". But, the test will perform this check right away. This causes a race condition, because the app may not have had time to receive the response and update the DOM by the time the assertion is run. A much better assertion would be:
This assertion will wait
Capybara.default_wait_time for the message text to equal
"Loaded successfully", giving our app time to process the request, and respond.
The final item we’ll look at are Capybara’s Actions. Actions provide a much nicer way to interact with elements on the page. They also take into account a few different edge cases that you could run into for some of the different input types. But in general, they provide a shortened way of interacting with the page elements, as the action will take care of performing the
Looking at the example above, we can re-write the test as such:
click_link will not just look for something on the page with the id of
#foo, it will restrict its search to a link. It will perform a
find, and then call click on the element that
If you write feature/acceptance tests using Capybara, then you should spend some time getting familiar with Capybara’s Finders, Matchers, and Actions. Learning how to use these APIs effectively will help you steer clear of flaky tests, saving you a whole lot of time and aggravation.