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

Integration Testing with Page Objects

Dorian Karter
September 15, 2016

Integration Testing with Page Objects

Dorian Karter

September 15, 2016
Tweet

More Decks by Dorian Karter

Other Decks in Programming

Transcript

  1. Give you an important tool for crafting maintainable, expressive integration

    tests. My goal today Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket
  2. Types of Testing Unit Tests Manual Tests Automated Integration Tests

    Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket
  3. Integration Tests Touch every layer of your application Emulate real

    user behavior Really good at setting up complex scenarios Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket
  4. But… Slow to run Harder to write & long setup

    Happy path mostly (Incomplete) Hard to read Repetitive Brittle Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket
  5. Integration Testing Tools Cucumber & Capybara RSpec & Capybara (on

    the Ruby on Rails ecosystem) Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket
  6. • Hard to maintain Gherkin • Hard to write Gherkin

    right on the fly (reuse is cu-cumbersome) • Gherkin breaks editors • Business people don’t actually use them… • Parsing natural language into code is a mental overhead Cucumber is OK, but: Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket
  7. RSpec does Integration Testing Better! R 3 feature 'User signs

    in' do 4 scenario 'with correct username and password' do 5 FactoryGirl.create(:user, email: '[email protected]', password: 'password') 6 7 visit login_path 8 9 within 'form' do 10 fill_in 'Username', with: '[email protected]' 11 fill_in 'Password', with: 'password' 12 click_on 'Login' 13 end 14 15 expect(page).to have_content 'Success' 16 end 17 end Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket
  8. But it gets messy quickly • TDD’s red, green, refactor

    is broken • what do I write next? • Tests are brittle • Hard to read (too low level, not declarative) R Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket
  9. feature 'Customer purchases an item' do scenario 'with saved payment

    information' do # .. setup visit product_path(product) expect(page).to have_selector('#productTitle', text: product.name) expect(page).to have_selector('#priceblock_ourprice', '$19.99') within '#buybox' { click_on 'Add to Cart' } upsell_items = all('.huc-first-upsell-row a.huc-upsell-max-2-lines').map(&:text) expect(upsell_items).to include(['Imploding Kittens', 'Cards Against Humanity']) within '#ewc' { click_on 'Proceed to Checkout' } expect(page).to have_selector(:css, '.header h1', text: 'Checkout') within '.shipping-speeds:first' { choose('two') } expect(page).to have_selector('.displayAddressUL', text: customer.shipping_address) expect(page).to have_selector('#subtotalsSection .grand-total-price', text: "$19.99") click_on 'Place your order' expect(page).to have_content('Thank you, your order has been placed') end end
  10. The Solution: Page Objects • Not a new idea -

    Martin Fowler’s Blog (WindowDriver 2004, PageObject 2013) • Popularized by the official Selenium documentation as a solution for reducing test maintenance and reducing code duplication. • Not very well known Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket
  11. Why Use Page Objects? • Write expressive test code •

    More conducive to TDD • Makes tests less brittle • DRY and reusable Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket
  12. How to Write PageObjects Create a PORO and include Capybara::DSL.

    Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket
  13. 1 module Pages 2 class Confirmation 3 include Capybara::DSL 4

    5 def on_page? 6 has_selector?('h1', text: ‘Checkout Confirmation') 7 end 8 end 9 end
  14. 1 # The following line is provided for convenience purposes.

    It has the downside 2 # of increasing the boot-up time by auto-requiring all files in the support 3 # directory. Alternatively, in the individual `*_spec.rb` files, manually 4 # require only the support files necessary. 5 # 6 # Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
  15. 1 # The following line is provided for convenience purposes.

    It has the downside 2 # of increasing the boot-up time by auto-requiring all files in the support 3 # directory. Alternatively, in the individual `*_spec.rb` files, manually 4 # require only the support files necessary. 5 # 6 Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
  16. 1 # The following line is provided for convenience purposes.

    It has the downside 2 # of increasing the boot-up time by auto-requiring all files in the support 3 # directory. Alternatively, in the individual `*_spec.rb` files, manually 4 # require only the support files necessary. 5 # 6 Dir[Rails.root.join(‘spec/support/pages/*.rb')].each { |f| require f }
  17. Free RSpec Matchers This is what allows us to create

    beautifully expressive tests def on_page? expect(confirmation_page).to be_on_page Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket
  18. Free RSpec Matchers This is what allows us to create

    beautifully expressive tests def has_cart_item_count?(amount) expect(confirmation_page).to have_cart_item_count(1) Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket
  19. On to the next page Click Integration Testing w/ Page

    Objects | @dorian_escplan | @hashrocket
  20. feature 'Customer purchases an item' do scenario 'with saved payment

    information' do # ... setup product_page = Pages::Product.new(product) product_page.visit_page expect(product_page).to be_on_page expect(product_page).to have_price('$19.99') product_page.click_add_to_cart confirmation_page = Pages::CartAddConfirmation.new expect(confirmation_page).to be_on_page expect(confirmation_page.upsell_items).to include(['Imploding Kittens’, # …]) confirmation_page.click_proceed_to_checkout checkout = Pages::Checkout.new expect(checkout).to be_on_page checkout.select_shipping(:free_two_day) expect(checkout).to have_shipping_address(customer.formatted_shipping_address) expect(checkout).to have_order_total('$19.99') checkout.click_place_order expect(checkout).to have_confirmation_message end end
  21. Failures: 1) Customer purchases an item with saved payment information

    Failure/Error: product_page = Pages::Product.new(product) NameError: uninitialized constant Pages::Product # ./spec/features/customer_purchases_items_spec.rb:7:in `block (2 levels) in <top (required)>' Run the test! $ rspec spec/features/customer_purchases_items_spec.rb
  22. 1) Customer purchases an item with saved payment information Failure/Error:

    product_page.visit_page NoMethodError: undefined method `visit_page' for #<Pages::Product:0x007fbe995be320 @product={}> # ./spec/features/customer_purchases_items_spec.rb:8:in `block (2 levels) in <top (required)>' Run the test, again! $ rspec spec/features/customer_purchases_items_spec.rb
  23. module Pages class Product include Capybara::DSL include CapybaraErrorIntel::DSL include Rails.application.routes.url_helpers

    def initialize(product) @product = product end def visit_page visit product_path(@product) end end end spec/support/pages/product.rb
  24. module Pages class Product include Capybara::DSL include CapybaraErrorIntel::DSL include Rails.application.routes.url_helpers

    def initialize(product) @product = product end def on_page? has_selector?('#productTitle', text: @product.name) end def has_price?(amount) has_selector?('#priceblock_ourprice', text: amount) end def visit_page visit product_path(@product) end end end
  25. So we went from this: before Page Objects Integration Testing

    w/ Page Objects | @dorian_escplan | @hashrocket
  26. feature 'Customer purchases an item' do scenario 'with saved payment

    information' do # .. setup visit product_path(product) expect(page).to have_selector('#productTitle', text: product.name) expect(page).to have_selector('#priceblock_ourprice', '$19.99') within '#buybox' { click_on 'Add to Cart' } upsell_items = all('.huc-first-upsell-row a.huc-upsell-max-2-lines').map(&:text) expect(upsell_items).to include(['Imploding Kittens', 'Cards Against Humanity']) within '#ewc' { click_on 'Proceed to Checkout' } expect(page).to have_selector(:css, '.header h1', text: 'Checkout') within '.shipping-speeds:first' { choose('two') } expect(page).to have_selector('.displayAddressUL', text: customer.shipping_address) expect(page).to have_selector('#subtotalsSection .grand-total-price', text: "$19.99") click_on 'Place your order' expect(page).to have_content('Thank you, your order has been placed') end end
  27. feature 'Customer purchases an item' do scenario 'with saved payment

    information' do # ... setup product_page = Pages::Product.new(product) product_page.visit_page expect(product_page).to be_on_page expect(product_page).to have_price('$19.99') product_page.click_add_to_cart confirmation_page = Pages::CartAddConfirmation.new expect(confirmation_page).to be_on_page expect(confirmation_page.upsell_items).to include(['Imploding Kittens’, # …]) confirmation_page.click_proceed_to_checkout checkout = Pages::Checkout.new expect(checkout).to be_on_page checkout.select_shipping(:free_two_day) expect(checkout).to have_shipping_address(customer.formatted_shipping_address) expect(checkout).to have_order_total('$19.99') checkout.click_place_order expect(checkout).to have_confirmation_message end end
  28. 1 module Pages 2 class PostsIndex 3 include Capybara::DSL 4

    5 def on_page? 6 has_selector?(:css, 'h1', text: 'Index') 7 end 8 end 9 end What about errors?
  29. $ rspec spec/features/user_signs_in_spec.rb:61 Run options: include {:locations=>{"./spec/features/user_signs_in_spec.rb"=>[61]}} Randomized with seed

    11643 F Failures: 1) User remains signed in when visiting root page (login) user is redirected to posts index Failure/Error: expect(posts_index).to be_on_page expected `#<Pages::PostsIndex:0x007fb71bbe4bf8>.on_page?` to return true, got false # ./spec/features/user_signs_in_spec.rb:60:in `block (2 levels) in <top (required)>' Finished in 0.52679 seconds (files took 2.47 seconds to load) 1 example, 1 failure Failed examples: rspec ./spec/features/user_signs_in_spec.rb:50 # User remains signed in when visiting root page (login) user is redirected to posts index Randomized with seed 11643
  30. 1 module Pages 2 class PostsIndex 3 include Capybara::DSL 4

    include CapybaraErrorIntel::DSL 5 6 def on_page? 7 has_selector?(:css, 'h1', text: 'Index') 8 end 9 end 10 end
  31. $ rspec spec/features/user_signs_in_spec.rb:61 Run options: include {:locations=>{"./spec/features/user_signs_in_spec.rb"=>[61]}} Randomized with seed

    28431 F Failures: 1) User remains signed in when visiting root page (login) user is redirected to posts index Failure/Error: has_selector?(:css, 'h1', text: 'Index') expected to find css "h1" with text "Index" but there were no matches. Also found "Posts", which matched the selector but not all filters. # ./spec/support/pages/posts_index.rb:7:in `on_page?' # ./spec/features/user_signs_in_spec.rb:60:in `block (2 levels) in <top (required)>' Finished in 0.50323 seconds (files took 3.01 seconds to load) 1 example, 1 failure Failed examples: rspec ./spec/features/user_signs_in_spec.rb:50 # User remains signed in when visiting root page (login) user is redirected to posts index Randomized with seed 28431
  32. CapybaraErrorIntel github.com/dkarter/capybara_error_intel Still WIP. Feedback, Issues, PRs and Stars are

    much appreciated! Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket
  33. Best Practices • No assertions in Page Objects • Know

    when to expose predicate methods • Prefer user language when modeling page objects • A page object does not have to represent a single page • Don't make a method on a page object do too much Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket
  34. Noteworthy Libraries natritmeyer/site_prism cheezy/page-object ngauthier/domino DSL for page objects dkarter/capybara_error_intel

    tpope/vim-projectionist Helpers Tooling Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket
  35. Conclusion • Page Objects are a great, more maintainable, readable

    and reusable abstraction • Try them out gradually by refactoring existing tests • Give the `include Capybara::DSL` approach a try first • CapybaraErrorIntel will let you enjoy the best of both worlds Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket