Pro Yearly is on sale from $80 to $50! »

Integration Testing with Page Objects

0e752ec9121eb5ebc9924f5b2e4b788e?s=47 Dorian Karter
September 15, 2016

Integration Testing with Page Objects

0e752ec9121eb5ebc9924f5b2e4b788e?s=128

Dorian Karter

September 15, 2016
Tweet

Transcript

  1. Integration Testing with PageObjects

  2. Dorian Karter Senior Developer @ @dorian_escplan doriankarter.com @dkarter hashrocket.com

  3. Survey Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket

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

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

    Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket
  6. 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
  7. 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
  8. Integration Testing Tools Cucumber & Capybara RSpec & Capybara (on

    the Ruby on Rails ecosystem) Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket
  9. • 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
  10. 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: 'test@example.com', password: 'password') 6 7 visit login_path 8 9 within 'form' do 10 fill_in 'Username', with: 'test@example.com' 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
  11. 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
  12. 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
  13. 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
  14. Page Objects? Integration Testing w/ Page Objects | @dorian_escplan |

    @hashrocket
  15. Page Objects? (semantically “workflow object”) Integration Testing w/ Page Objects

    | @dorian_escplan | @hashrocket
  16. 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
  17. How to Write PageObjects Create a PORO and include Capybara::DSL.

    Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket
  18. 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
  19. Where Page Objects Live spec/support/pages directory For example: spec/support/pages/checkout_confirmation.rb Integration

    Testing w/ Page Objects | @dorian_escplan | @hashrocket
  20. 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 }
  21. 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 }
  22. 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 }
  23. 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
  24. 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
  25. Let’s Test Drive a Familiar Application Integration Testing w/ Page

    Objects | @dorian_escplan | @hashrocket
  26. Navigate to product page Integration Testing w/ Page Objects |

    @dorian_escplan | @hashrocket
  27. On to the next page Click Integration Testing w/ Page

    Objects | @dorian_escplan | @hashrocket
  28. Click Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket

  29. You gonna buy that?

  30. Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket

  31. 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
  32. 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
  33. First good red!

  34. module Pages class Product include Capybara::DSL def initialize(product) @product =

    product end end end spec/support/pages/product.rb
  35. 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
  36. Now we’re really moving!

  37. 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
  38. 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
  39. And so on…

  40. So we went from this: before Page Objects Integration Testing

    w/ Page Objects | @dorian_escplan | @hashrocket
  41. 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
  42. To this: after Page Objects Integration Testing w/ Page Objects

    | @dorian_escplan | @hashrocket
  43. 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
  44. 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?
  45. $ 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
  46. CapybaraErrorIntel github.com/dkarter/capybara_error_intel ~30 LOC Integration Testing w/ Page Objects |

    @dorian_escplan | @hashrocket
  47. 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
  48. $ 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. You can find the slides on doriankarter.com/talks hashrocket.com | doriankarter.com

    | @dorian_escplan | github.com/dkarter Thank You