#391 Testing JavaScript with PhantomJS pro
- Download:
- source codeProject Files in Zip (57 KB)
- mp4Full Size H.264 Video (27.3 MB)
- m4vSmaller H.264 Video (12.3 MB)
- webmFull Size VP8 Video (13.5 MB)
- ogvFull Size Theora Video (28.5 MB)
Below is a page from a Rails application that handles adding an order. The first field on the form accepts a credit card number and if we enter an invalid number in to it we’ll see an error message as there’s some JavaScript on that page that performs a mod 10 check to validate the number when the field loses the focus. If we enter a valid number into the field the error message disappears when we shift the focus.
The JavaScript that performs this check is a little complicated so it’s a good idea to test it at a lower level like we showed in episode 261. That we should also add some high-level tests to test that the code fits in properly with the rest of the application.
class @CreditCard constructor: (number) -> @number = number.replace(/[ -]/g, '') validNumber: -> total = 0 for i in [(@number.length-1)..0] n = +@number[i] if (i+@number.length) % 2 == 0 n = if n*2 > 9 then n*2 - 9 else n*2 total += n total % 10 == 0 $.fn.validateCreditCardNumber = -> @each -> $(this).blur -> card = new CreditCard(@value) if !card.validNumber() $(this).next('.error').text("Invalid card number.") else $(this).next('.error').text("")
We’ll add a request spec to ensure that this code is well integrated. We already have RSpec and Capybara set up in our gemfile and an empty request spec where we’ll write the test for validating a credit card number. If you’re unfamiliar with request specs take a look at episode 257 which covers this topic. Our first test will visit the “new order” page, fill in the credit card number field with an invalid number then check that the page has the content “Invalid card number”.
require 'spec_helper' describe "Orders" do it "validates card number" do visit new_order_path fill_in "Credit Card Number", with: "1234" page.should have_content("Invalid card number") end end
When we run this spec it fails. At this point we’d usually do some test-driven development to implement this functionality but in this case the implementation has already been done so the test should pass. The reason this spec is failing is that request specs don’t execute JavaScript by default and we’ll need to change this for it to pass. Capybara integrates nicely with JavaScript so all we have to do is add a js: true
option to the spec.
it "validates card number", js: true do visit new_order_path fill_in "Credit Card Number", with: "1234" page.should have_content("Invalid card number") end
Using PhantomJS
When this test runs now it will execute the JavaScript using Selenium which will start up Firefox and fill in the credit card field on the form. The test still fails, however, and this is because the validation is triggered by the blur
event on the text field and the Selenium driver doesn’t fire this event when it fills it in. There are workarounds to get this working but instead we’ll use a different JavaScript driver for Capybara. Selenium is a little slow and needs to use Firefox so we’ll replace this with PhantomJS. This runs a headless version of WebKit and will allow us to test our application’s JavaScript without opening a browser window. PhantomJS can be used for a number of other things too but in this episode we’ll just be using it for automated testing. We can download PhantomJS from its website or, if we’re running Mac OS X install it with Homebrew by running this command.
$ brew install phantomjs
After we’ve installed PhantomJS we’ll have access to a phantomjs
command. If we run this command without passing any arguments to it it opens up an interactive console. Using this can be a little cumbersome so we’ll write a script for it to run instead. We can use JavaScript for CoffeeScript for this file so we’ll use CoffeeScript.
page = require('webpage').create() page.open 'http://localhost:3000', (status) -> console.log "Status: #{status}" phantom.exit()
It’s common when using PhantomJS to call require('webpage').create()
to return a new page object and we do that here and assign the result to a variable. We then call open on this and pass it a URL. When the page has loaded it triggers a callback function that takes a status
argument and for now we’ll just print this value to the console and then exit from script. To run the script we pass the file to the phantomjs
command. When we do it visits our Rails application in the background and returns success
as we expect.
$ phantomjs try_phantom.coffee Status: success
The real power comes in executing the page’s JavaScript which we can do by calling page.evaluate
and passing in a function. The code in the function will be executed in context of the page that we’re currently on. As an example we’ll use this to print out the page’s title.
page = require('webpage').create() page.open 'http://localhost:3000', (status) -> title = page.evaluate -> document.title console.log "Title: #{title}" phantom.exit()
When we run this script we’ll see the title of our application’s home page.
$ phantomjs try_phantom.coffee Title: Orders
Using Poltergeist
There’s a lot more we can do when using PhantomJS directly and the API Reference has more details. To use it as a driver for Capybara we can use the Poltergeist gem which does just what we need. Setting it up is straightforward: we just add it to the test group in the gemfile then run bundle
to install it.
gem 'rspec-rails', group: [:test, :development] group :test do gem 'capybara' gem 'launchy' gem 'poltergeist' end
As we’re using RSpec we’ll modify the spec helper file next and add these two lines below the other require
statements.
require 'capybara/poltergeist' Capybara.javascript_driver = :poltergeist
When we run our specs again now they’re run in the background without a browser window showing and this time they pass. It seems that Poltergeist does trigger the blur
event when it fills in the field.
We now have a convenient way to ensure that our JavaScript behaviour is well integrated with our Rails application. We wouldn’t use this to test any complex logic or edge cases, for that we’d be better off using Jasmine or something else that works at a lower level.
Another Test
What if we want to try testing something a little more difficult? We have a page that shows a list of orders which uses endless pagination like we used in episode 114.
When we scroll to the bottom of the page the text “Fetching more orders...” appears until the next page of results is fetched from the server via an AJAX request. We’ll use PhantomJS to test this behaviour by adding another test to our orders spec.
it "fetches more orders when scrolling to the bottom", js: true do 11.times { |n| Order.create! number: n+1 } visit orders_path page.should have_content('Order #1') end
Our orders page displays ten items per page by default so we first create eleven orders in this test and give each one a number from 1 to 10. We then visit the orders page and check that the text “Order #1” appears there. When we run this test it fails and the error message suggests that the page doesn’t contain any of our orders. The issue here is that each test is run in a separate database transaction. If we look in the spec helper file we’ll see that there’s a use_transactional_fixtures
setting set to true
. This doesn’t work with PhantomJS because this runs in a separate process which uses a separate database connection and so won’t use the same data as the test suite.
To get around this we could set this value to false
and clear the database in a different way such as using the Database Cleaner gem like we did in episode 257. An alternative solution is presented by José Valim on the Plataformatec blog. There he demonstrates some code to share the database connection across processes which means that PhantomJS will the same database records as our tests. This means that we can continue to use transactions for each test which will make the test suite run more quickly. We’ll add this code to a new file in the /spec/support
directory.
class ActiveRecord::Base mattr_accessor :shared_connection @@shared_connection = nil def self.connection @@shared_connection || retrieve_connection end end # Forces all threads to share the same connection. This works on # Capybara because it starts the web server in a thread. ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection
When we run our specs now they both pass as PhantomJS can see the database records. Next we’ll expand on our last test, adding a check to see that the eleventh order isn’t displayed on the page as it should only show the first ten when the page first loads. We then use evaluate_script
to run some JavaScript that will scroll to the bottom of the page and then check that the eleventh order is then shown.
it "fetches more orders when scrolling to the bottom", js: true do 11.times { |n| Order.create! number: n+1 } visit orders_path page.should have_content('Order #1') page.should_not have_content('Order #11') page.evaluate_script("window.scrollTo(0, document.height)") page.should_not have_content('Order #11') end
When we run our specs now they still both pass so it appears that our endless scrolling behaviour is working. That said it’s not the best situation when tests stay green after they’ve been modified. We want to be sure that the scrolling is being tested correctly so we’ll comment out the code that fetches the next page of orders when we scroll to the bottom of the page. When we do this we get a failing spec so we can uncomment the code again knowing that it’s being tested correctly.
Speeding Up The Test Suite
Running these JavaScript specs can be a little slow so we could consider not running them every time we run our test suite. We can skip the JavaScript specs and run the others by running this command.
$ rspec . --tag '~js' Run options: exclude {:js=>true}
This won’t run any specs for our application as all the specs we have are JavaScript specs but it’s a useful tip, especially for application with large test suites.
Poltergeist isn’t the only way to run PhantomJS. There are a number of other projects available depending on which test framework we’re using. A list of these can be found here.