Testing Ember.js with Capybara and RSpec
What’s the problem?
Capybara is awesome for integration tests and has a great API. Even with relatively complex js on the page, just setting js:true does the trick and everything just works and you get to test your entire UI, js or not.
However if your rails app implements Ember on the frontend you may have experienced what’s technically referred to as, a hell ride.
Why Ember hurts and common exceptions
It doesn’t really. You can have issues testing js with Capybara with any js code, Ember or not.
The thing about Ember though is that it’s asynchronous by design and implementation, really asynchronous, and that can lead to weird behaviour with Capybara + Rspec. Namely, the most common errors you’re likely to encounter are:
1. Capybara::ElementNotFound.
2. Some variation of a database is locked exception.
The first exception happens because Ember templates haven’t loaded parts of the UI yet.
The second exception actually happens because your Capybara tests have finished but Ember is still doing some async ajax action, and the database locks up when your database cleaner tries to run.
Additionally, you may have found that a quick fix to stop these exceptions is to litter your test code with sleep commands. This of course is bad. It’s prone to breaking AND it slows down our tests.
Our goal then is reliable, fast, and intuitive Capybara specs when implementing asynchronous Ember.js code.
Here’s how
1. Use .find() instead of .first() or .all()
Capybara has great built in support for waiting for elements to appear in the UI before throwing Capybara::ElementNotFound exceptions. However because .find will throw Capybara::Ambiguous exceptions we sometimes like to use .all or .first.
Don’t use these. .first and .all don’t use Capybara’s clever wait feature and we need this in async js code.
Find a way to make your UI elements unique somehow so you can use .find.
2. Explicitly wait for Ajax
Sometimes in our specs we want to check the database to make sure the correct data has been saved, or we may want to make sure the ajax operation has completed before moving on in the spec.
The wait_for_ajax method below (all credit to Ryan Bigg for this one) plugs into jQuery’s $.active property to make sure all ajax operations are complete. Luckily Ember uses jQuery for it’s ajax. Phew.
#credit: http://ryanbigg.com/2013/07/waiting-for-ajax-in-capybara/
def wait_for_ajax
counter = 0
while true
active = page.execute_script(“return $.active”).to_i
#puts “AJAX $.active result: “ + active.to_s
break if active < 1
counter += 1
sleep(0.1)
raise “AJAX request took longer than 5 seconds OR there was a JS error. Check your console.” if counter >= 50
end
end
NOTE 1: If you’re using Poltergeist (I’m using Selenium) page.execute_script will not work. In this case use page.evaluate_script as in:
active = page.evaluate_script(“$.active”).to_i
I’ve tested the above with :poltergeist, :selenium and :webkit and actually it work on all three, so I’d use that instead of page.execute_script.
Thanks to @jlevesy for pointing this out!
NOTE 2: wait_for_ajax is pretty cool as it fails if there are any js errors on the page. So this catches js errors too.
3. Make your Ember code only as async as it needs to be, use .then()
Yes Ember’s design is inherently asynchronous. However sometimes order matters and it’s ok to introduce some blocking calls. That’s what .then is there for, don’t be scared to use it if an operation depends on another operation before running.
Implementing .then wisely will result in code that is easier to debug and more predictable to test.
4. Wait for Ajax before Database.clean
Sometimes your spec interacts with the UI but doesn’t care about what’s happened in the server. This is fine for the spec, but could cause a Deadlock error when the Database Cleaner runs, as we’ve mentioned before. This is because the cleaner will run while a database operation is still running from an ajax request that was triggered by a spec.
So, before cleaning the database, we need to wait for all request to finalise.
#/spec/spec_helper.rb
config.after(:each, :js => true) do
wait_for_ajax
DatabaseCleaner.clean
end
That’s it!
There’s not too much to it, but sometimes it’s easy to get caught out on the small stuff.
Implementing these 4 guidelines should fix all your Ember + Capybara spec issues. If it doesn’t drop me a line on twitter @thatandyrose.