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

One Page to Test Them All!

Embed Size (px)

Citation preview

A Cross platform mobile automation framework

Created & Presented by, Priti Biyani & Aroj George

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

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

AUTOMATION

๏ Android

Calabash Android

๏ iOS

Calabash iOS

๏ Mobile Web

Watir Webdriver

4

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

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

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

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

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

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?

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 NAME

13

Page

Name : String

To Refer from feature file

Login Page

Name :“Login”

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 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?

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

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

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

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

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

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

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

TRANSITION ELEMENT

24

@login_button = TransitionElement.new(

{

:web => '#continue_button',

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

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

},

{

:to => HomePage,

}

)

Next Page Class

TRANSITION ELEMENT - MULTIPLE TRANSITION

25

Login

Admin Home Page

Member Home Page

userType?[success]

TRANSITION ELEMENT - MULTIPLE TRANSITION

@login_button = TransitionElement.new(

{

:web => '#continue_button',

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

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

},

{

:to => [AdminPage, UserPage],

}

) Multiple Transition

TRANSITION ELEMENT - ERROR TRANSITION

Login

Admin Home Page

Member Home Page

userType?

[ SUCCESS ]

[ FAIL ]

TRANSITION ELEMENT - ERROR TRANSITION

@login_button = TransitionElement.new(

{

:web => '#continue_button',

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

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

},

{

:to => [AdminPage, UserPage],

:error => Login

}

)

Error Representation

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.

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 OBJECT EXAMPLE - SEAT MAP

31

Native Implementation

Common code across platform

Iphone Mobile Web Android

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 |

SEATMAP PAGE

33

SeatMap

Name :“Seat map”Id : {… }

select_seat(pax, flight, seat_number)

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

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

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

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

PRIMARY AND SECONDARY NAVIGATION

40

Iphone Mobile Web Android

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? ()

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

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)

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

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

FUTURE DIRECTION

‣ Debug Mode

‣ Log debug info like element id, clicks etc…

‣ Slow element locator finder

46

LEARNINGS

‣ QA/Dev Pairing

‣ Use case driven development

‣ Evolution

47

SUMMARY

๏ Good OO design is the key

๏ Good abstractions can help solve hard problems

48

CONCLUSION

49

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

just one place.

Reach out to us at

Aroj George @arojpPriti Biyani @pritibiyani

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

THANK YOU