Why does adding "sleep 1" in an after hook cause this Rspec/Capybara test to pass?
Asked Answered
D

2

7

I'm using rails 4.0.5, rspec 2.14.1, capybara 2.2.1, capybara-webkit 1.1.0 and database_cleaner 1.2.0. I'm seeing some weird behavior with the following feature test (which simulates a user viewing a comment on a post, hovering over an icon to make a menu appear, and clicking a menu item to delete the comment):

let(:user){create(:user)}
let(:post){create(:post, author: user)}
let!(:comment){create(:comment, post: post, author: user)}

...

it "can delete a comment" do
  assert(page.has_css? "#comment-#{comment.id}")
  find("#comment-#{comment.id}-controls").trigger(:mouseover)
  find("#comment-#{comment.id} .comment-delete a").click
  assert(page.has_no_css? "#comment-#{comment.id}")
end

This test fails about 80% of the time, always due to some record being retrieved from the database as nil-- I get NoMethodError: undefined method X for nil:NilClass, for various values of X. Sometimes the nil is the comment that's being deleted, sometimes it's the post that the comment's attached to, sometimes it's the author of the comment/post.

If I add sleep 1 to the end of the test, it passes:

it "can delete its own comment" do
  assert(page.has_css? "#comment-#{comment.id}")
  find("#comment-#{comment.id}-controls").trigger(:mouseover)
  find("#comment-#{comment.id} .comment-delete a").click
  assert(page.has_no_css? "#comment-#{comment.id}")
  sleep 1
end

It also passes if I put sleep 1 in an after block.

Any idea why I get these NoMethodErrors, and/or why the test passes if I make it sleep for a second after all the work is done?

Douzepers answered 9/5, 2014 at 0:32 Comment(2)
What line of code are the NoMethodErrors springing from? It’s weird that putting sleep at the end fixes it, but Capybara race conditions are a common problem.Lenalenard
@BuckDoyle All the errors say Failure/Error: Unable to find matching line from backtrace, followed by references to various model/controller methods that get called by an AJAX request that's triggered when a comment is deleted. Looks like it's a race condition, not between the Capybara assertion and the comment deletion but between the database teardown and the callback code that runs after the deletion.Douzepers
C
5

I suspect that it's possible in your application for the comment to disappear from the page (which is the last thing you're asserting) before it's deleted from the database. That means that the test can clean up before the deletion happens. If this is the case you can fix it by waiting for the actual deletion to happen at the end of the test. I have this method around (a reimplementation of a method that was removed from Capybara 2 but is still sometimes necessary)

def wait_until(delay = 1)
  seconds_waited = 0
  while ! yield && seconds_waited < Capybara.default_wait_time
    sleep delay
    seconds_waited += 1
  end
  raise "Waited for #{Capybara.default_wait_time} seconds but condition did not become true" unless yield
end

so I can do

wait_until { Comment.count == 0 }

in tests.

Another approach is to add Rack middleware that blocks requests made after the test ends. This approach is described in detail here: http://www.salsify.com/blog/tearing-capybara-ajax-tests I saw it do a very good job of addressing data leakage in a good-sized suite of RSpec feature specs.

Cordes answered 9/5, 2014 at 2:31 Comment(2)
Actually the comment always gets deleted before disappearing, but the disappearance triggers an AJAX callback that (I think) is racing with the database cleanup. The approach you linked looks promising; I'll try it out. Thanks!Douzepers
Capybara.default_wait_time is now deprecated in favor of Capybara.default_max_wait_timeSeduction
U
0

https://github.com/jnicklas/capybara#asynchronous-javascript-ajax-and-friends

When working with asynchronous JavaScript, you might come across situations where you are attempting to interact with an element which is not yet present on the page. Capybara automatically deals with this by waiting for elements to appear on the page.

Underside answered 9/5, 2014 at 2:4 Comment(2)
This doesn’t explain why putting sleep at the end fixes the problem, after any assertions have been made. That’s why I asked the original poster for more detail.Lenalenard
But does say that you should not use sleep. Why would you deliberately slow down your tests. Capybara deals with waiting for elements really well as per phoetIntendant

© 2022 - 2024 — McMap. All rights reserved.