On my last project, we initially had a lot of trouble writing reliable end-to-end tests. Over time, we improved reliability, readability and usefulness by changing the way we approached end-to-end testing and learning more about Capybara.
Let’s look at some loosely-based-on-reality examples. We had a feature that was similar to a web forum. One person could create a post, which other people could see in a list and reply to. We wanted to test that the posts we had automagically created were appearing as expected in the list. Initially, we wrote something like this:
posts = all('.post')
expect(posts[0].text).to eq 'First post heading'
expect(posts[1].text).to eq 'Second post heading'
And so on. This looked neat at first – we could assert that the posts appeared with the correct text in the correct order and that posts we intended to hide were indeed hidden. But there was a problem – we could continue retrieving or rendering posts after invoking Capybara’s all finder, in which case those posts were unintentionally excluded from our results. We needed to wait until all of our posts had been retrieved and rendered, but Capybara didn’t know what to wait for – it doesn’t know how many posts there are supposed to be. The result of the spec depended on how fast our EC2 instance had rendered the text in question, how quickly the page loaded and how long it took to retrieve posts from the API.
Perhaps this approach isn’t well supported because it isn’t really necessary. Does a user in this scenario ever expect the list of posts to explicitly exclude a particular post? If we want to test that, we can test it at a lower level – perhaps by using a posts controller spec to verify that a scope is being applied. The same is true for ordering. Provided you have good unit test coverage, a good approach might be to test a simpler case:
post = find('.post')
expect(post.text).to eq 'Only post heading'
Now that Capybara has a single thing to look for, we can use the find finder to wait for that thing to appear so that our test is less sensitive to variations in timing. You can read more about Capybara finders here. But we might still have problems. If you are using Angular – as we were – sometimes it might take a little time for your text to render correctly. The above approach will wait for the element to exist, but it will compare the actual text to the expected text immediately which may yield inconsistent results. Instead, we could try:
post = find('.post')
expect(post).to have_text 'Only post heading'
Now that we’re using a finder with an implicit wait and a matcher with an implicit wait, Capybara will wait for the element to appear, then wait again for its text to match the expected value. You can read more about Capybara matchers here.
We’ve already made big improvements to the reliability of our tests, but the thinking behind them is still a bit funny. What we really want to test is the interaction – the part that goes end to end, from user to database. We need to find a post on the page only so that we can click on it and read it, just as a user would. We don’t need to meticulously inspect all content on the page – that could be done more efficiently in a view test. What if we just did this?
post = find('.post', text: 'Only post heading')
post.click
But is the selector really necessary? Sometimes you might need to check something very specific, but often that is better done in a lower level test and the specific thing will have specific content and thereby be unique on the page anyway. Capybara has an action for clicking on links, so we might even end up with:
click_link 'Only post heading'
Now we’re using a Capybara action. Capybara actions generally have an implicit wait and help us write more readable, interaction focused tests.
Let’s look at another example, like responding to a post. Perhaps this is a simple case and we can apply our current approach: Find something specific using a finder with an implicit wait, assert on its content using a matcher with an implicit wait and use actions to keep complicated selectors out of our spec. That might look something like this:
fill_in 'Add your response', with: 'Response content' click_button 'Submit' expect(page).to have_text '1 response' expect(page).to have_text 'Response content'
But your case might be too complex for something like that. For example, you might have another submit button on the page. In this case, you might be tempted to go back to a complicated selector:
find('.post .post__new-post-response button').click
But this isn’t necessary. We can use method chaining to scope our calls to Capybara’s actions. For example, we could write a selector for the whole response form component, pull it out into a module or function and then write this:
ResponseForm.fill_in 'Add your response', with: 'Response content' ResponseForm.click_button 'Submit'
A lot of this falls out naturally from writing end-to-end tests from a users perspective. Our users don’t sleep(1) after they click a button; they don’t give up if their content takes half a second to load; they don’t use CSS classes to decide which button to click or where to look on the page for some text they just entered. Now our specs don’t either.
Here are some key ideas that have been useful to us:
- Try to write end-to-end tests from the user’s perspective
- Find and match using Capybara’s implicitly waiting
findersandmatchers, to avoid timing sensitivity - Use
actions - Keep selectors out of your spec
