Upload
thoughtworks
View
496
Download
0
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
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 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 |
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
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
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