Top Banner
A Cross platform mobile automation framework Created & Presented by, Priti Biyani & Aroj George ONE PAGE TO TEST THEM ALL
50

One Page to Test Them All!

Jul 15, 2015

Download

Technology

ThoughtWorks
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: One Page to Test Them All!

A Cross platform mobile automation framework

Created & Presented by, Priti Biyani & Aroj George

ONE PAGE TO TEST THEM ALL

Page 2: One Page to Test Them All!

U.S. AIRLINE MOBILE APP

๏ iOS, Android, iPad, Blackberry, Windows and Mobile Web

๏ Apple Appstore featured app (Travel)

2

Page 3: One Page to Test Them All!

CALATRAVA

Custom open source cross platform javascript framework - Calatrava.

https://github.com/calatrava/calatrava

‣ Pure native screens

‣ Pure HTML

‣ HTML + Native widgets

‣ Native + HTML Webviews

3

Page 4: One Page to Test Them All!

AUTOMATION

๏ Android

Calabash Android

๏ iOS

Calabash iOS

๏ Mobile Web

Watir Webdriver

4

Page 5: One Page to Test Them All!

PROBLEM STATEMENT

5

Create a generic automation framework which could support each of the

three UI automation tools and work seamlessly across both native and

hybrid UI screens

Page 6: One Page to Test Them All!

REQUIREMENTS

๏ Should make it easy to add automation. It should make life of QA easy!

๏ Allow reuse of code and help avoid duplication

๏ Promote higher level business/domain oriented thinking instead of low level UI

interactions

๏ Make changes in only one place.

๏ Once automation is done for a feature on one platform, adding the same for other

platforms should be trivial.

๏ Keep it simple, just use basic OO concepts.

6

Page 7: One Page to Test Them All!

SOLUTION ANALYSIS

๏The Page Object Model Pattern

๏Requirements satisfied

✓Domain oriented thinking

✓Code reuse/no duplication

✓ Changes in only one place.

✓ Easy to add automation.

7

Page 8: One Page to Test Them All!

PAGE OBJECT MODEL SECOND THOUGHTS

๏ ~ 60 screens on iOS and Android

๏ ~ 30 screens on Mobile Web

๏ 60 x 2 platforms + 30 = 150 page object classes!

๏ Screens have similar behavior and expose same services

‣Requirements check

✓Allows reuse of code and helps avoid duplication.

✓Changes are required to be done in only one place.

✓Makes it easy to add automation.

✓Promotes higher level business/domain oriented thinking

➡ So clearly this approach is not scalable.

8

Page 9: One Page to Test Them All!

COMMON PAGE OBJECTS PER SCREEN

๏ page objects per screen will solve the class explosion problem.

๏ Requirements satisfied

✓ Should make it easy to add automation. It should make life of QA easy!

✓ Allow reuse of code and help avoid duplication!

✓ Promote higher level business/domain oriented thinking instead of low level UI interactions!

✓ Make change in only one place.

✓ Once automation is done for a feature on one platform, adding the same for other

platforms should be trivial.

9

Page 10: One Page to Test Them All!

COMMON PAGE OBJECTS - CONCERNS?

๏ Different automation tool APIs

Element query syntax (xpath/css vs custom calabash syntax)

๏ Different UI actions

click/tap on web/mobile

๏ Different platform implementations for same screen

locator values can vary

๏ UX interactions patterns

for e.g Nav Drawer in Android, the Tab Bar in iOS and the Nav bar in web.

10

Page 11: One Page to Test Them All!

COMMON PAGE OBJECTS - MORE CONCERNS ?

๏ No online example of this approach.

๏ Calabash reference uses a different Page Object class for iOS and Android.

11

➡ So is it really possible to have common page objects?

Page 12: One Page to Test Them All!

OBJECT MODELING

Lets try and do a Object Modeling exercise!

Single page object class

➡ What are the key fields?

➡ What’s the key behavior?

12

Page 13: One Page to Test Them All!

PAGE NAME

13

Page

Name : String

To Refer from feature file

Login Page

Name :“Login”

Page 14: One Page to Test Them All!

PAGE IDENTIFIER

14

Page

Name : StringId : Map

Identify each page uniquely on the device

{

:web => "//div[@id='payment_info']",

:ios => "all webView css:'#payment_info'",

:droid => "all webView css:'#payment_info'"

}

Page 15: One Page to Test Them All!

PAGE ID CLASS

15

PageId

Id : Map

exists? ()

Check if locator specified by id exists on the UI

Calabash Android and IOS:

not query(locator).empty?

Mobile Web (Watir):

Browser.element(:css => locator).present?

Page 16: One Page to Test Them All!

ID EXISTS CHECK

16

def exists?

case platform

when ANDROID

query(ui_query).empty?

when IOS

query(ui_query).empty?

when WEB

Browser.element(:css => locator).present?

end

end

Page 17: One Page to Test Them All!

DRIVER ABSTRACTION

17

Page

Name : StringId : PageId

PageId

Field : Map

exists? ()

PageId

Driver

Platform : droid

exists? ()

Driver

Platform : ios

exists? ()

Driver

Platform : web

exists? ()

def exists?(id_map)

locator = id_map[:droid]

begin

opts.merge!(:screenshot_on_error => false)

wait_for_elements_exist([locator],opts)

rescue WaitError

false

end

end

def exists?(id_map)

locator = id_map[:ios]

begin

opts.merge!(:screenshot_on_error => false)

wait_for_elements_exist([locator], opts)

element_exists locator

rescue WaitError

false

end

end

def exists?(id_map, wait=true)

locator = id_map[:web]

begin

Browser.element(:css => locator).wait_until_present if wait

Browser.element(:css => locator).present?

rescue TimeoutError

puts "exists error #{locator}"

false

end

end

Page 18: One Page to Test Them All!

ELEMENTS

18

Page

Name : StringId : PageIdElements

Element.new({

:web => ‘#save_and_continue_button’,

:ios => "navigationButton marked:'DONE'",

:droid => "* marked:'Done'"

}

Element

Id: Map

click ()

def click

case platform

when ANDROID

touch(query(locator))

when IOS

touch(query(locator))

when WEB

Browser.element(:css => locator).click()

end

end

Page 19: One Page to Test Them All!

ELEMENTS DELEGATE TO DRIVER

19

Page

Name : StringId : MapElements

Element

Id : Map

Driver : Driver

click()

Driver

Platform : droid

exists? click()

Driver

Platform : ios

exists? click()

Driver

Platform : web

exists? click()

def click(id_map)

locator = id_map[:droid]

begin

scroll_to locator

rescue RuntimeError

end

touch(query(locator))

wait_for_loader_to_disappear

end

def click(id_map)

locator = id_map[:ios]

begin

scroll_to locator

rescue RuntimeError

end

touch(query(locator))

wait_for_loader_to_disappear

end

def click(id_map)

locator = id_map[:web]

B.element(:css => locator).wait_until_present

B.element(:css => locator).click

wait_for_loader_to_disappear

end

Page 20: One Page to Test Them All!

MORE ELEMENTS

20

Page

Name : StringId : PageIdElement 1: ElementElement 2: Element….

Element

Driver

Platform : droid

exists? click() setText()getText()checked()

Textbox

setText()getText()

id : map

Checkbox

checked?()check(item)

id : map

Dropdown

select(item)

id : map

Id : MapDriver : Driver

Driver

Platform : ios

exists? click() setText()getText()checked()

Driver

Platform : web

exists? click() setText()getText()checked()

exists? ()

Page 21: One Page to Test Them All!

PAGE TRANSITION

21

Login Home Page

def login

click_login_button

wait_for_home_page_to_load

return HomePage.new

end

[ success ]

def <action>

click_button

wait_for_<next_page>_to_load

return <NextPage.new>

end

Page 22: One Page to Test Them All!

TRANSITION MODELING

1. driver.click

✓ Driver should be responsible only for UI interaction.

✓ Driver should not know about higher level abstraction Page.

2. element.click

✓ It will not be applicable to all elements.

3. Transition Aware Element

➡ An element that understands page transitions.

22

Page 23: One Page to Test Them All!

TRANSITION ELEMENT

23

Page

Name : StringId : PageIdElements

Element

Driver

Platform : droid

exists? click() setText()getText()checked()

Textbox

setText()getText()

id : map

Checkbox

checked?()check(item)

id : map

Dropdown

select(item)

id : map

Id : MapDriver : Driver

Driver

Platform : ios

exists? click() setText()getText()checked()

Driver

Platform : web

exists? click() setText()getText()checked()

exists? ()

TransitionElement

click(item)

id : map

Page 24: One Page to Test Them All!

TRANSITION ELEMENT

24

@login_button = TransitionElement.new(

{

:web => '#continue_button',

:ios => "* marked:'Login'",

:droid => "* marked:'Login'"

},

{

:to => HomePage,

}

)

Next Page Class

Page 25: One Page to Test Them All!

TRANSITION ELEMENT - MULTIPLE TRANSITION

25

Login

Admin Home Page

Member Home Page

userType?[success]

Page 26: One Page to Test Them All!

TRANSITION ELEMENT - MULTIPLE TRANSITION

@login_button = TransitionElement.new(

{

:web => '#continue_button',

:ios => "* marked:'Login'",

:droid => "* marked:'Login'"

},

{

:to => [AdminPage, UserPage],

}

) Multiple Transition

Page 27: One Page to Test Them All!

TRANSITION ELEMENT - ERROR TRANSITION

Login

Admin Home Page

Member Home Page

userType?

[ SUCCESS ]

[ FAIL ]

Page 28: One Page to Test Them All!

TRANSITION ELEMENT - ERROR TRANSITION

@login_button = TransitionElement.new(

{

:web => '#continue_button',

:ios => "* marked:'Login'",

:droid => "* marked:'Login'"

},

{

:to => [AdminPage, UserPage],

:error => Login

}

)

Error Representation

Page 29: One Page to Test Them All!

MULTIPLE TRANSITIONS - WRONG TRANSITION

‣ Scenario: If there is a bug in code, app transitions to wrong page from list of multiple transitions.

‣ Should transition element detect this bug?

✓ but this is application logic✓ will increase complexity

➡ Tests should take care for assertions of correct page, not the framework.

Page 30: One Page to Test Them All!

TRANSITION ELEMENT

30

def wait_till_next_page_loads(next_pages, error_page)

has_error, found_next_page = false, false

begin

wait_for_element(timeout: 30) do

found_next_page = next_pages.any? { |page| page.current_page?}

has_error = error_page.has_error? if error_page

found_next_page or has_error

end

has_error ? error_page : next_pages.find { |page| page.current_page?}

rescue WaitTimeoutError

raise WaitTimeoutError, "None of the next page transitions were found. Checked for: =>

#{next_page_transitions.join(' ,')}"

end

end

Page 31: One Page to Test Them All!

PAGE OBJECT EXAMPLE - SEAT MAP

31

Native Implementation

Common code across platform

Iphone Mobile Web Android

Page 32: One Page to Test Them All!

SEAT MAP FEATURE STEP

32

And I select following seat

| origin | destination | flight_number | pax_name | seat_number |

| ORD | JFK | DL3723 | Rami Ron | 9C |

| ORD | JFK | DL3723 | John Black | 10C |

| JFK | ATL | DL3725 | Rami Ron | 12C |

| JFK | ATL | DL3725 | John Black | 13C |

Page 33: One Page to Test Them All!

SEATMAP PAGE

33

SeatMap

Name :“Seat map”Id : {… }

select_seat(pax, flight, seat_number)

Page 34: One Page to Test Them All!

SEAT MAP FEATURE STEP

34

And(/^I select following seat$/) do |table|

table.hashes.each do |row|

flight = Flight.new(row['origin'], row['destination'], row[‘flight_number’])

seat_map_page = SeatMap.new

seat_map_page.select_seat row['pax_name'], flight, row['seat_number']

end

end

And I select following seat

| origin | destination | flight_number | pax_name | seat_number |

| ORD | JFK | DL3723 | Rami Ron | 9C |

| ORD | JFK | DL3723 | John Black | 10C |

| JFK | ATL | DL3725 | Rami Ron | 12C |

| JFK | ATL | DL3725 | John Black | 13C |

Page 35: One Page to Test Them All!

PAGE OBJECT EXAMPLE

35

class SeatMap < Page

def initialize

@id = PageId.new({

:web => "//div[@id='ism']//div[@id='sb_seat_map_container']",

:ios => "webView css:'#seat_map_container'",

:droid => "webView css:'#seat_map_container'"

})

@leg = Field.element({

:web => "label[@class='dd-option-text'][text()='%s: %s to %s']",

:ios => "label marked:'%s: %s to %s'",

:droid => "* marked:'%s: %s to %s'"

})

@passenger = Field.element({

:web => "//label[@class='dd-option-text'][text()='%s']",

:ios => "label marked:'%s'",

:droid => "* marked:'%s'"

})

@passenger_seat_number_text = Field.element({

:web => "//label[@class='dd-selected-sec-text'][text()='%s']",

:ios => "label {text CONTAINS '%s'}",

:droid => "* marked:'%s'"

})

@seat_button = Field.element({

:web => “//div[@id='seat_%s']",

:ios => "webView css:'#seat_%s'",

:droid => "webView css:'#seat_%s'"

})

Page 36: One Page to Test Them All!

PAGE OBJECT EXAMPLE

36

class SeatMap < Page

def select_seat_for_pax_for_leg(flight_leg, pax_name, seat_number)

select_leg flight_leg

select_passenger pax_name

select_seat seat_number

end

def select_leg(flight_leg)

@leg_header.click

@leg.click(flight_leg.flight_number, flight_leg.origin, flight_leg.destination)

end

def select_passenger(pax_name)

@passenger_name_header.click

@passenger.click(pax_name)

end

def select_seat(seat_no)

@seat_button.click(seat_no)

@passenger_seat_number_text.await(seat_no)

end

end

"label marked:'%s: %s to %s'"

"label marked:'%s'"

"webView css:'#seat_%s'"

Page 37: One Page to Test Them All!

TRANSITION TO NEXT PAGE

37

class SeatMap < Page

def initialize

@done_button = Field.transition_element({

:web => "#save_and_continue_button",

:ios => "navigationButton marked:'DONE'",

:droid => "* marked:'Done'"

},

{

:to => [SeatsDetails],

})

super(‘Seat Map')

end

def close

@done_button.click

end

end

Page 38: One Page to Test Them All!

TRANSITION TO SEATS DETAILS PAGE

38

def verify_seat_details seat_details_page, table

table.hashes.each do |row|

flight = Flight.new(row['origin'], row['destination'], row['flight_number'])

actual_seat_number = seat_details_page.get_seat_number(row[‘pax_name'], flight)

expected_seat_number = row['seat_number']

expect(actual_seat_number).to eq(expected_seat_number)

end

end

And(/^I select following seat$/) do |table|

table.hashes.each do |row|

flight = Flight.new(row['origin'], row['destination'], row[‘flight_number’])

seat_map_page = SeatMap.new

seat_map_page.select_seat row['pax_name'], flight, row['seat_number']

end

seat_details_page = seat_map_page.close

verify_seat_details seat_details_page, table

end

Page 39: One Page to Test Them All!

COMMON PAGE OBJECTS - SOLUTION

๏ Different automation tool APIs/ Different UI actions

➡ Driver abstraction

๏ Different platform implementations for same screen

➡ Element locator map.

๏ UX interactions patterns (Nav Drawer / Tab bar /Menu)

➡ How?

39

Page 40: One Page to Test Them All!

PRIMARY AND SECONDARY NAVIGATION

40

Iphone Mobile Web Android

Page 41: One Page to Test Them All!

MENU/MENU ITEM

41

Menu

Name : StringId : Map

MenuItems

MenuItem

Name : String

TargetPage : Page

Type : MenuType

MenuButton:TransitionElement

show()show_secondary_menu()launch (item_name)

def launch(item_name)

show

menu_item = @menu_items[item_name]

show_secondary if menu_item.is_secondary?

menu_item.click

end

click ()is_secondary? ()

Page 42: One Page to Test Them All!

MENU IMPLEMENTATION

42

class Menu

SHOP_AND_BOOK = 'Book'

MY_TRIPS = 'My Trips'

FLIGHT_STATUS = 'Flight Status’

def initialize()

@id = PageId.new({

:web => "//div[@id='home']//ul[@id='home_options']",

:ios => "tabBarButtonLabel marked:'Book'",

:droid => "* id:'drawer_items_list'"

})

@primary_menu_button = Field.element ({ web: ‘#menu', :ios => '', :droid => "* id:'home'" }),

@secondary_menu_button = Field.element ({ web: '', :droid => '', :ios => "* marked:'More'" }),

@menu_items = {

SHOP_AND_BOOK => MenuItem.new(SHOP_AND_BOOK, FlightSearch),

MY_TRIPS => MenuItem.new(MY_TRIPS, MyTrips),

FLIGHT_STATUS => MenuItem.new(FLIGHT_STATUS, FlightStatus, ios: MenuItem::SECONDARY)}

end

def show

@primary_menu_button.click

end

def show_secondary

@secondary_menu_button.click

end

def launch(item_name) . . . end

end

Page 43: One Page to Test Them All!

MENU ITEM IMPLEMENTATION

43

class MenuItem

PRIMARY = 1

SECONDARY = 2

attr_accessor :name, :page, :type

def initialize(name, page, type = PRIMARY)

@name = name

@page = page

@type = type

@menu_button = Field.transition_element(

{

:web => "//li[@class='#{@name.downcase}']",

:ios => "label marked:'#{@name}'",

:droid => "WhitneyDefaultTextView id:'drawer_item_text' text:'#{@name}'"

},

{

:to => @page

})

end

def click

@menu_button.click

end

def is_secondary?

@type[Driver.platform] == SECONDARY

end

end

#

MenuItem.new(FLIGHT_STATUS, FlightStatus, ios: MenuItem::SECONDARY)

Page 44: One Page to Test Them All!

COMMON PAGE OBJECTS - SOLUTION

✓ Different automation tool APIs

➡ Driver abstraction

✓ Different UI actions

➡ Driver abstraction

✓ Different platform implementations for same screen

➡ Element locator map.

✓ UX interactions patterns (Nav Drawer / Tab bar /Menu)

➡ Menu and Menu Item Abstraction

44

Page 45: One Page to Test Them All!

IMPLEMENTATION NOTES

‣ We "require" specific driver class during test execution.

‣ Page registry, can be queried for page object instances.

‣ PageRegistry.get "Login Page”

‣ Rspec Unit Tests

‣ Rake task to create new page classes.

45

Page 46: One Page to Test Them All!

FUTURE DIRECTION

‣ Debug Mode

‣ Log debug info like element id, clicks etc…

‣ Slow element locator finder

46

Page 47: One Page to Test Them All!

LEARNINGS

‣ QA/Dev Pairing

‣ Use case driven development

‣ Evolution

47

Page 48: One Page to Test Them All!

SUMMARY

๏ Good OO design is the key

๏ Good abstractions can help solve hard problems

48

Page 49: One Page to Test Them All!

CONCLUSION

49

Today, to add automation to support new feature development across the three different platforms requires changes in

just one place.

Page 50: One Page to Test Them All!

Reach out to us at

Aroj George @arojpPriti Biyani @pritibiyani

http://arojgeorge.ghost.io/http://pritibiyani.github.io/

THANK YOU