Share
Embed
One Page to test them all!
This is talk Aroj(https://www.linkedin.com/pub/aroj-george/b/573/74b) and I presented at vodQA, Bangalore.
Normally when we have cross platform to support, we end up defining multiple pages for each of the platform irrespective of similar service provided by each. When we came across this situation, we decided to give it chance for having single page which will represent all platforms. This presentation addresses same thing.
You can find more details about the talk and details over here:
https://pritibiyani.github.io/blog/speaking-at-vodqa-banglore
Priti
April 25, 2015
Transcript
50 slides
-
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 : String Id : 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 : String Id : 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 : String Id : PageId Elements
Element.new({ :web => ‘#save_and_continue_button’, :ios => "navigationButton marked:'DONE'", :droid => "* marked:'Done'" } Element Id: Map click () 18 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 : String Id
: Map Elements Element Id : Map Driver : Driver click() 19 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 : String Id : PageId
Element 1: Element Element 2: Element …. Element 20 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 : Map Driver : Driver Driver Platform : ios exists? click() setText() getText() checked() Driver Platform : web exists? click() setText() getText() checked() exists? () -
PAGE TRANSITION 21 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 : String Id : PageId
Elements Element 23 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 : Map Driver : 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 25 Login Admin Home
Page Member Home Page userType? Adm in M em ber [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? Adm in [ SUCCESS ] [ FAIL ] M em ber -
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 "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 : String Id : Map
MenuItems MenuItem Name : String TargetPage : Page Type : MenuType MenuButton:TransitionElement show() show_secondary_menu() launch (item_name) 41 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 -
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 @arojp Priti Biyani
@pritibiyani http://arojgeorge.ghost.io/ http://pritibiyani.github.io/ THANK YOU