Sharing Page Object classes and Test scenarios between Android and iOS

Tech Writers at PF

Sharing Page Object classes and Test scenarios between Android and iOS

How we merged iOS and Android Page Objects classes and Test scenarios

Hello “Bug Hunters” and everyone keen to learn more about how we can fight the good fight in QA!

At Property Finder we pay a lot of attention to the automation part of testing so having best practices in our development process is a must. While implementing our tests for a mobile project we faced a few issues that needed to be improved.

Originally we had separate classes for iOS and Android; we literally had the same code duplicated for iOS and Android page objects methods and tests. It’s not exactly ‘wrong’ to use it this way but we found it to be a bit complex, especially when we have five different apps to automate: it certainly would confuse future test maintenance, and make parallel testing difficult. Also, when it comes to best practices in code quality, duplicating the same code is not the best approach and since IOS & Android UI and functionality of app’s are similar we decided to improve our framework by having methods that allow us to use same Page Object classes and Test scenarios for both — iOS and Android — together.

Here’s how we did it

We are using mostly same test cases, methods and functionality between Android and iOS in our apps, so the main difference is to identify the objects on the page. That is why we defined both platforms object identifiers in the common page.

1. Created a method that picks the right selector based on the platform using default types

We needed a method that would choose the right selector to use while running the test. By having the following selector structure:

element :selector, ‘<android selector>’ , ‘ <ios selector>’

We implemented a method as follows:

def self.find_platform_locator(*find_args)
  platform = ENV['PLATFORM'].upcase
 
#If Android - it will remove second element and use 1st as a locator in a test
  if (find_args.length == 2) && (platform == 'ANDROID')
    find_args.delete_at(1)
   
#If iOS - it will remove first element and use second as a locator in a test
  elsif (find_args.length == 2) && (platform == 'IOS')
    find_args.delete_at(0)
  end
 
#If the element is only iOS or Android specific and it is using non-default selector (example :class) then the following code is used:
  if find_args[0].split(':').length == 2
    selectors = find_args[0].split(':')
   
#will return class:android.widget
    return selectors[0].to_sym, selectors[1]
   
#If the default selector type used - then it will be used based on platform
 else
    return [BasePage.get_default_selector(platform), find_args[0]]
  end
end


find_platform_locator.rb hosted with ❤ by GitHub                                                                          view raw

2. Native methods override

Since we are using Site Prism for our page objects to make all of the above work — native Site Prism methods located in DSL module had to be overridden:

#inherited DSL module
   Include SitePrism::DSL

#single
def self.element(name, *find_args)
   m_find_args = find_platform_locator(*find_args)
   super(name, *m_find_args)
end
	
#multiple elements
def self.elements(name, *find_args)
   m_find_args = find_platform_locator(*find_args)
   super(name, *m_find_args)
end

override.rb hosted with ❤ by GitHub                                                                                                 view raw

3. How it looks on Page Object class

With the default selector type it looks like:

element :selector_label, 'android label' , 'name == "ios label"'

With the non-default selector it looks like (For Android we have class: selector type and for iOS — xpath://) :

element :button,'class:android.widget.ImageButton',''xpath://XCUIElementTypeStaticText[@name="Section']

As previously mentioned in section 1 — we can also have a selector only for Android or only for iOS — it will still work and use it based on the platform.

elements :platform_specific_element, 'android_button_selector'

4. Identified default selector

We implemented a “Base Page” that all other page classes inherit. Instead of having method for a selector type that might not even get used, we defined a default selector type. In our apps most elements have the types ‘predicate’ for iOS and ‘id’ for Android so we set it as a default in methods:

def self.get_default_selector(platform)
   if platform.downcase == 'android'
     return :id
   elsif platform.downcase == 'ios'
     return :predicate
   else
     raise ArgumentError, 'Wrong Platform Name'
   end
end


get_default_selector.rb hosted with ❤ by GitHub                                                                               view raw

5. Created a variable for default type in Android and iOS

Instead of mentioning it in the method we created a variable with a default selector type per platform:

@@selector = { ANDROID: :id, IOS: :predicate }

#after adding variable modification
def self.get_default_selector(platform)
  if platform.downcase == 'android'
     return @@selector[:ANDROID]
  elsif platform.downcase == 'ios'
     return @@selector[:IOS]
  else
     raise ArgumentError, 'Wrong Platform name'
  end
end


get_default_selector_with_variable.rb  hosted with ❤ by GitHub                                                        view raw

And changed the class variable:

def self.set_default_selector(android, ios)
  @@selector = { ANDROID: android, IOS: ios }
end

class_variable.rb hosted with ❤ by GitHub                                                                                         view raw

Benefits we have got:

1. Easier maintenance

  • if a selector is changed, added or removed you just go to same Page Object and change it for both
  • if the test logic is changed you just make a change in the same class without jumping through folders between Android and iOS

2. Time saved on test implementation

  • instead of duplicating the same code for each platform we only have to write one class
  • keeping logic and test cases common for both platforms
  • having customized methods allows us to not mention the common (default) selector type when identifying selectors

3. Instead of keeping everything separate we now have the following structure:

AppName
|->pages
|	|->BasePage.rb
|	|->page1.rb
|	|->page2.rb
|->features
|	|->test1.rb
|	|->test2.rb

Instead of

AppName
|->Android
|	|->pages
|	|	|->page1.rb
|	|	|->page2.rb
|	|->features
|	|	|->test1.rb
|	|	|->test2.rb
|->iOS
|	|->pages
|	|	|->page1.rb
|	|	|->page2.rb
|	|->features
|	|	|->test1.rb
|	|	|->test2.rb

Which is a lot easier to handle!

You can find BasePage.rb here

That is it! Thanks for reading

If you liked this article and want to be part of our brilliant team of engineers that produced it, then have a look at our latest vacancies here.