Slide 1

Slide 1 text

Integration Testing with PageObjects

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

Give you an important tool for crafting maintainable, expressive integration tests. My goal today Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket

Slide 5

Slide 5 text

Types of Testing Unit Tests Manual Tests Automated Integration Tests Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

Integration Testing Tools Cucumber & Capybara RSpec & Capybara (on the Ruby on Rails ecosystem) Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket

Slide 9

Slide 9 text

• 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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

Page Objects? Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

How to Write PageObjects Create a PORO and include Capybara::DSL. Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Where Page Objects Live spec/support/pages directory For example: spec/support/pages/checkout_confirmation.rb Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket

Slide 20

Slide 20 text

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 }

Slide 21

Slide 21 text

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 }

Slide 22

Slide 22 text

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 }

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Let’s Test Drive a Familiar Application Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

You gonna buy that?

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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 ' Run the test! $ rspec spec/features/customer_purchases_items_spec.rb

Slide 33

Slide 33 text

First good red!

Slide 34

Slide 34 text

module Pages class Product include Capybara::DSL def initialize(product) @product = product end end end spec/support/pages/product.rb

Slide 35

Slide 35 text

1) Customer purchases an item with saved payment information Failure/Error: product_page.visit_page NoMethodError: undefined method `visit_page' for # # ./spec/features/customer_purchases_items_spec.rb:8:in `block (2 levels) in ' Run the test, again! $ rspec spec/features/customer_purchases_items_spec.rb

Slide 36

Slide 36 text

Now we’re really moving!

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

And so on…

Slide 40

Slide 40 text

So we went from this: before Page Objects Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

To this: after Page Objects Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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?

Slide 45

Slide 45 text

$ 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 `#.on_page?` to return true, got false # ./spec/features/user_signs_in_spec.rb:60:in `block (2 levels) in ' 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

Slide 46

Slide 46 text

CapybaraErrorIntel github.com/dkarter/capybara_error_intel ~30 LOC Integration Testing w/ Page Objects | @dorian_escplan | @hashrocket

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

$ 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 ' 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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

You can find the slides on doriankarter.com/talks hashrocket.com | doriankarter.com | @dorian_escplan | github.com/dkarter Thank You