Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Front-end Testing for Skeptics

Front-end Testing for Skeptics

Paul Graham once quipped that "Web 2.0" really meant "JavaScript now works". Nearly ten years later, more and more functionality of our web applications is written in JavaScript. But for those of us who came of age when JavaScript was unreliable, it's been preferable to test the server-side, while leaving the UI a thin-as-possible shell. Testing the front-end was error prone and brittle.

However, when you're delivering a JavaScript widget hundreds of thousands of times a day on diverse third party websites, it needs to work. So: we need to test it, and those tests need to be as painless as possible to write and maintain.

This is a session for front-end testing skeptics (like me): It is possible to create tests that drive your web UI (JavaScript and all) that are automated, fast, reliable, headless -- no browser required -- and written in pure Ruby instead of some obtuse syntax. We'll explore the challenges and gotchas of testing the front-end and walk through an example that meets the above goals.

Luke Francl

April 29, 2013
Tweet

More Decks by Luke Francl

Other Decks in Programming

Transcript

  1. M A N N I N G Ben Vinegar Anton

    Kovalyov FOREWORD BY Paul Irish
  2. 8/12 9/12 10/12 11/12 12/12 1/13 2/13 3/13 4/13 Search

    Volume Conveniently missing Y axis
  3. 8/12 9/12 10/12 11/12 12/12 1/13 2/13 3/13 4/13 Total

    Search Engines Conveniently missing Y axis
  4. Scenario: Testing front-end code Given that I am a programmer

    And that I can code Ruby Then writing tests in English makes me sad
  5. # Gemfile group :test, :development do gem 'rspec-rails', '~> 2.13'

    gem 'capybara', '~> 2.1' gem 'poltergeist', '~> 1.2' end github.com/look/frontend-testing-for-skeptics
  6. # spec_helper.rb require 'capybara/rspec' require 'capybara/poltergeist' Capybara.register_driver :poltergeist do |app|

    Capybara::Poltergeist::Driver.new(app, :js_errors => true, :inspector => true) end Capybara.javascript_driver = :poltergeist github.com/look/frontend-testing-for-skeptics
  7. describe 'JavaScript errors', :js => true do context 'home page'

    do it 'does not raise a JavaScript error' do visit root_path fill_in "country", :with => 'Uni' end end end Minimum Viable Test
  8. describe 'JavaScript errors', :js => true do context 'home page'

    do it 'does not raise a JavaScript error' do visit root_path fill_in "country", :with => 'Uni' end end end Minimum Viable Test
  9. it 'shows off the Capybara DSL' do visit '/path' #

    use text or DOM ID click_on('Login') fill_in('Username', :with => 'RailsConf') page.should have_css('button.red') # Finders wait and retry # if the element isn't found find('button.red').click # Run JavaScript in the page context page.evaluate_script("1 == '1'").should be_true end
  10. describe 'country autocomplete', :js => true do it 'shows autocompletions

    and redirects' do visit root_path fill_in "country", :with => 'norw' find("body").click # HACK find('.tt-suggestion').click current_path.should == country_path('NO') end end
  11. 1) country autocomplete with match shows autocompletions and redirects to

    the selected country Failure/Error: find('.tt-suggestion').click Capybara::ElementNotFound: Unable to find css ".tt-suggestion"
  12. describe 'country autocomplete', :js => true do it 'shows no

    results' do visit root_path fill_in "country", :with => 'zzz' # Sometimes it may be easier to # use JavaScript directly! js = '$(".tt-suggestions").children().size() == 0' page.evaluate_script(js).should be_true end end
  13. RSpec::Matchers.define :have_clean_global_scope do |*allowed| match do |event_proc| # globals_js is

    a string containing a JavaScript function # that returns an array of global variable names before_globals = page.evaluate_script(globals_js) event_proc.call after_globals = page.evaluate_script(globals_js) diff = after_globals - before_globals # ignore objects in allowed diff = diff - allowed diff.empty? end end
  14. describe 'xss', :js => true do let(:title) do "Title <script>window.xss

    = true;</script>" end let(:document) { Document.create(:title => title) } it "escapes JavaScript" do visit document_path(document) find("h1").should have_content(title) page.evaluate_script('window.xss').should_not be_true end end
  15. it "should show autocomplete without leaking globals" do # force

    capybara to wait until the script gets added to the DOM page.find("script[src*='/assets/swiftype']") page.find("link[rel='stylesheet'][href*='/assets/swiftype']") wait_until { page.evaluate_script('typeof $stjq === "undefined"') == false } expect do click_on '#st-search-input' # wait for click before filling in the field sleep(1) fill_in 'st-search-input', :with => 'content' wait_for_ajax!('$stjq') wait_until { page.has_css?('.swiftype-widget div.autocomplete ul li') } end.to have_clean_global_scope end Capybara 1
  16. Capybara 2 it "should show autocomplete without leaking globals" do

    page.find("script[src*='/embed.js']") page.find("script[src*='/assets/swiftype']") page.find("link[rel='stylesheet'][href*='/assets/swiftype']") page.current_scope.synchronize do if page.evaluate_script("typeof $stjq === 'undefined'") raise Capybara::ExpectationNotMet.new("Script wasn't true") end end expect do click_on '#st-search-input' fill_in 'st-search-input', :with => 'content' page.has_css?('.swiftype-widget div.autocomplete ul li') end.to have_clean_global_scope end
  17. # Use Capybara's waiting mechanism until a # JavaScript expression

    evaluates to true. def synchronize_javascript(script) current_node.synchronize do unless evaluate_script(script) raise ExpectationNotMet.new("Script wasn't true") end end end Kyle VanderBeek http://bit.ly/YrBG7t
  18. Transactions and database setup Some Capybara drivers need to run

    against an actual HTTP server. Capybara takes care of this and starts one for you in the same process as your test, but on another thread. Selenium is one of those drivers, whereas RackTest is not. – Capybara README
  19. Next steps: JavaScript unit testing describe("recursive factorial", function() { it("is

    correct", function() { expect(factorial(10)).toBe(3628800); }); });
  20. Full stack integration testing with Rails 3, Cucumber, RSpec, QUnit

    and Capybara Matthew O'Riordan, April 17, 2011 blog.mattheworiordan.com/post/4701529828/
  21. $ teabag Starting the Teabag server... Teabag running default suite

    at http://127.0.0.1:52681/(...) FFF Failures: 1) Factorial without memoization base case is correct Failure/Error: Expected 0 to be 1. 2) Factorial without memoization recursion case is correct Failure/Error: Expected 0 to be 3628800. 3) Factorial with memoization has the same result as the (...) Failure/Error: Expected 3628800 to be 0. Finished in 0.00500 seconds 3 examples, 3 failures