“Concurrency is a bitch” this famous quote by any developer ever can haunt your everyday work like crazy. Why? Because mainly race-conditions and concurrency does not fit the “normal” way of thinking about code, which normally works like a cursor that processes stuff sequentially.
This week this quote haunted a test-suite of a fellow startup. Using rspec
and capybara-webkit
a few tests were failing with errors similar to these:
Failure/Error: Unable to find matching line from backtrace
ActionView::Template::Error:
undefined method `<some method>' for nil:NilClass
# ./app/helpers/some_helper.rb:9:in `<something_fetched_from_the_database>'
or
ActiveRecord::RecordNotFound:
Couldn't find SomeObject with 'id'=1
In both situations there was no obvious reason for the tests to fail: The requested objects were created in matching before
-blocks with factory_girl
. Most notably all of these specs were feature- / or acceptance-tests that used capybara-webkit
to click through a given scenario and check the page for all the expected elements.
Keep your spirit and database clean
Now when searching for the cause I noticed something strange: When the given example started running and the main expectations were checked, the later missing object was always present.
scenario "something should work" do
object = create(:object, user: user, related_objects: [create(:related_object)])
as_user(user).visit object_path(object.id)
expect(page).to have_content object.some_content
end
So meaning up until the expect(page) ...
the problematic objects (in this case related_objects
) were still present in the database. Still the spec was failing.
This meant a few things:
-
something
of the spec was still running when thescenario
ended -
something
was still considered a failure of the wholescenario
, when it threw an exception -
something_else
was deletingthe object
needed bysomething
from the database whilescenario
was still running, causing the failure - But since
the object
was still present in the database at the end ofscenario
,something_else
must be an element of outside of thescenario
and thescenario
must have been spawningsomething
implictly
To make matters short:
-
something
= An AJAX-request that was spawned by the page visited by the Capybara-headless browser -
something_else
= DatabaseCleaner a little gem that most of us use to clean up after every spec
Leave no AJAX-request behind
By adding a few debug-statements I noticed that database_cleaner
actually cleaned the database before the AJAX-request concluded.
The little gem was configured like this:
config.before(:each, :js => true) do
DatabaseCleaner.strategy = :truncation
end
config.after(:each) do
DatabaseCleaner.clean
end
This means that after every capybara-webkit
-driven example the database gets wiped clean by recreating every table.
Leaving you with the following neat situation:
What to do?
You got two options:
Stub away the empty request to the database if you can. For example:
allow(SomeObject).to receive(:find).and_return(double('SomeObject'))
or
Wait for the AJAX-request to complete before leaving the example.
Either by making Capybara wait for the AJAX-request with expect(page).to have_content(<something AJAXy>)
.
Or by using a wait_for_ajax
at the end of your example. If you are on Capybara 2.0 already, you can find one here.
So that you final example looks something like this:
scenario "something should work" do
object = create(:object, user: user, related_objects: [create(:related_object)])
as_user(user).visit object_path(object.id)
wait_for_ajax
expect(page).to have_content object.some_content
end