Python Appium tests on different Android versions

Problem: UI element identifiers are not consistent across mobile platforms.

Mobile is eating the world. However mobile operating systems are not yet fully mature. For example, UI element identifiers change between versions of the same OS. They also look different across Android and iOS. This causes automation scripts to be brittle. We hit this problem recently. We were writing automation for an app to run against different versions of Android. We quickly realized that the way to identify UI elements differed on each version of Android. The rest of this post details the solution we implemented to solve this problem.


Why this post?

Qxf2’s top goal is to help testers. We identified mobile automation as one area where we could help testers. Over the past few months, we have written several basic tutorials that help testers with mobile automation. We feel like the next stage is to address some common intermediate level problems you may face as you implement mobile automation at your workplace. Over the next few posts, we will tackle some common technical challenges testers are likely to face and our approach to solve them.


New to mobile automation?

If you are new to mobile automation, we suggest the following posts to get started:
a) Run Appium on an emulator
b) Run Appium on mobile devices
c) Identify UI elements on mobile platforms
d) Execute different mobile gestures


Overview

We will use BigBasket as the application under test over the next few posts. BigBasket is one of India’s largest online food and grocery store. The app is free to download and is sufficiently complex for us to illustrate many common technical problems you will face when automating workflows on mobile devices. For this post, we will show you how to login to the app on two different Android devices running Android 4.2 and Android 4.4. We noticed Android 4.2 did not have the id attribute for elements. We had to identify elements based on xpath. Whereas in Android 4.4, we could identify most of the elements directly using id.

At a high level, these are the steps we will perform
1. Write a ‘Base’ class
2. Create a configuration file
3. Accept the OS version as a command line parameter
4. Load the identifiers you need based on OS version
5. Build a logic to use the right method to find the UI element
6. Write the test

Appium_Test
An approach to running mobile automation scripts on multiple devices

1. Write a ‘Base’ class

We have chosen to use the facade pattern to keep things simple. We have written a Base module that contains a small library of methods which can be used by different tests. This helps separating the test case from the objects and methods in app to different files. The Base class will contain all the methods for actions which can be performed in the app like click element, get element, login, logout etc. For the purposes of this test, let us pretend that the name of our Base file is BigBasket_Base_Appium.py and that Base has only these methods.

"""
Qxf2 Base Appium script for BigBasket Android app
"""
import unittest,time, sys, os, dotenv
from appium import webdriver
from appium.webdriver.common.touch_action import TouchAction
from appium.webdriver.common.multi_action import MultiAction
from optparse import OptionParser
from ConfigParser import SafeConfigParser
 
class BigBasket_Base_Appium(unittest.TestCase):
    "Base class for BigBasket's Appium tests"
    def __init__(self,config_file,test_run):
        "Constructor"
        #test_run is the section name you use in the configuration file
        #For example, we have used Android_4.4 and Android_4.2
        self.test_run = test_run 
 
        #More to be filled in later in the tutorial
 
    def set_text(self,ui_identifier,value):
        "Set the value of the text field"
        flag = False
        if value is not None:
            text_field = self.get_element(ui_identifier)
            if text_field is not None:
                try:
                    text_field.clear()
                    text_field.send_keys(value)
                    flag = True
                except Exception, e:
                    print 'Exception when setting the text field: %s'%ui_identifier
 
        return flag
 
    def click_element(self,xpath):
        "Click the button supplied"
        flag = False
        try:
            link = self.get_element(xpath)
            if link is not None:
                link.click()
                flag = True
        except Exception,e:
                print 'Exception when clicking element with xpath: %s'%xpath
                print e
        return flag
 
    def get_element(self,path):
        "Return the DOM element for the xpath Or id or class name provided"
        #To be filled out later in the tutorial
        pass

2. Create a configuration file

Let us create a configuration file that uses ‘Android_4.x’ to identify sections and list the desired configuration under it. For our example we created a file called configuration.ini. We added the contents below in the configuration file for different version of Android OS. In this example we are showing you two ‘test_runs’ (Android_4.2 and Android_4.4) using different configurations.

[Android_4.2]
platformName = Android
platformVersion = 4.2
deviceName = Samsung
appPackage = com.bigbasket.mobileapp
appActivity = .activity.SplashActivity
user_title = android.widget.TextView
main_menu_icon = //android.widget.ImageView[@bounds='[408,59][456,107]']
signin_link = //android.widget.TextView[@text='Sign In']
signout_link = //android.widget.TextView[@text='Sign Out']
login_email = //android.widget.EditText[@bounds='[15,242][465,290]']
login_password = //android.widget.EditText[@bounds='[15,337][465,385]']
 
[Android_4.4]
platformName = Android
platformVersion = 4.4
deviceName = Moto E
appPackage = com.bigbasket.mobileapp
appActivity = .activity.SplashActivity
user_title = android.widget.TextView
main_menu_icon = com.bigbasket.mobileapp:id/settingImageHome
signin_link = com.bigbasket.mobileapp:id/signin
signout_link = com.bigbasket.mobileapp:id/signup
login_email = com.bigbasket.mobileapp:id/emailsig
login_password = com.bigbasket.mobileapp:id/pwdsig

3. Accept the OS version as a command line parameter

Let us have our script (different from Base) accept command line parameters to accept the configuration file and test run name. For our example we created a file called BigBasket_Login_Appium.py. The code snippet to accept the configuration file and command line is given below.

if __name__=='__main__':
    #Accept some command line options from the user
    #We used Python module optparse 
    usage = "usage: %prog -c  -t  \nE.g.1: %prog -c D:\\BigBasket\\configuration.ini -t \"Android_4.2\"\n---"
    parser = OptionParser(usage=usage)
    parser.add_option("-c","--config",dest="config_file",help="The full path of the configuration file")
    parser.add_option("-t","--test_run",dest="test_run",help="The name of the test run")
    (options,args) = parser.parse_args()

4. Load the identifiers you need based on OS version

We have chosen to use the module ConfigParser to read our configuration file. We can modify the constructor of the Base class. Modify the __init__ method in BigBasket_Base_Appium.py as follows

    def __init__(self,config_file,test_run):
        "Constructor"
        #test_run is the section name you use in the configuration file
        #For this example, we have used Android_4.4 and Android_4.2
        self.test_run = test_run 
 
        #Read the config file
        self.config_file = config_file
        parser = SafeConfigParser() 
        parser.read(self.config_file)
 
        #Desired capabilities can be read from the config file
        desired_caps = {}
        desired_caps['platformName'] = parser.get(self.test_run, 'platformName')
        desired_caps['platformVersion'] = parser.get(self.test_run, 'platformVersion')
        desired_caps['deviceName'] = parser.get(self.test_run, 'deviceName')
 
        # Make sure you install the BigBasket app using Play Store and then launch it directly using package and activity name
        desired_caps['appPackage'] = parser.get(self.test_run, 'appPackage')
        desired_caps['appActivity'] = parser.get(self.test_run, 'appActivity')
        self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
        self.action = TouchAction(self.driver)
 
        #UI identifiers start here 
        self.user_title = parser.get(self.test_run, 'user_title')
        self.main_menu_icon = parser.get(self.test_run, 'main_menu_icon')
        self.signin_link = parser.get(self.test_run, 'signin_link')
        self.login_email = parser.get(self.test_run, 'login_email')
        self.login_password  = parser.get(self.test_run, 'login_password')
        self.submit_button = parser.get(self.test_run, 'submit_button')

5. Build a logic to use the right method to find the UI element

This is a key step. Appium lets you find elements in different ways. For example you can find elements by xpath, class name or id. Let us pretend that one version of Android can identify elements using ids while another version of Android can identify elements only by xpath. Bummer! It looks like you need one script that calls find_element_by_id and another script that calls find_element_by_xpath. We have chosen to solve this by writing a (somewhat) intelligent wrapper to dynamically decide the right call to make. There are multiple ways to solve this issue. We decided to use a somewhat hokey, but straight forward method. Modify the get_element method in BigBasket_Base_Appium.py as follows

    def get_element(self,path):
        "Return the DOM element for the xpath Or id or class name provided"
        dom_element = None
        try:
            if ('//'in path):
                dom_element = self.driver.find_element_by_xpath(path)
            elif (':id/'in path):
                dom_element = self.driver.find_element_by_id(path)
            else:
                dom_element = self.driver.find_element_by_class_name(path)
        except Exception,e:
            print 'Exception while getting the path: %s'%path
            print e
        return dom_element

NOTE: The above simplistic check for xpath is not our favorite method but it has worked surprisingly well for us.

6. Write the test

Now you can complete BigBasket_Login_Appium.py. The test will call the required methods in the base class. The base class will use the configuration file to get the location of the elements.

"""
Qxf2 test case to login to BigBasket.
"""
 
from BigBasket_Base_Appium import BigBasket_Base_Appium
import os,time
from optparse import OptionParser
 
 
def run_login_test(config_file,test_run):
    "Example test"
    USERNAME = 'YOUR USERNAME'
    PASSWORD = 'YOUR PASSWORD'
    FIRSTNAME = 'YOUR FIRSTNAME'
 
    # Login to BigBasket using valid credentials
    test_obj = BigBasket_Base_Appium(config_file=config_file,test_run=test_run)
    test_obj.login(username=USERNAME, password=PASSWORD, firstname=FIRSTNAME)
 
 
#---START OF SCRIPT
if __name__=='__main__':
    #Accept some command line options from the user
    #Python module optparse 
    usage = "usage: %prog -c  -t  \nE.g.1: %prog -c $ Path of configuration.ini -t \"Android_4.2\"\n---"
    parser = OptionParser(usage=usage)
    parser.add_option("-c","--config",dest="config_file",help="The full path of the configuration file")
    parser.add_option("-t","--test_run",dest="test_run",help="The name of the test run")
    (options,args) = parser.parse_args()
 
    #Create a test obj with parameters
    run_login_test(config_file=options.config_file,test_run=options.test_run)

Run the test:
Running Login Test


There you have it! A working example of running the exact same Appium automation script against multiple Android versions. We want to emphasize that this is one way to solve this problem and not the only way to solve this problem. Our sincere hope is that, in a few years, mobile operating systems mature and testers will not have to jump through hoops to maintain automation scripts across different versions of the operating systems.

As always, leave your questions and comments below!


Subscribe to our weekly Newsletter


View a sample



11 thoughts on “Python Appium tests on different Android versions

  1. Hi Avinash,

    Can you show me an example of login function that you wrote on the base class? I tried to copy your script but test_obj.login shows unresolved attribute.

    Thank You

    1. Hi Yonathan,
      The reason we didn’t provide all the function in base class is that we want the users to try to write their own functions. Below is the code snippet of the login function which i wrote. You may still need to add some element identifiers or some dependent functions properly for this code to work

      def login(self, username, password, firstname):
      “Login using credentials provided in the env file”
      # Assert that the landing page contains text ‘Hi’
      login_success = False
      user_title_text = self.get_element(self.user_title).get_attribute(‘text’)

      if (user_title_text == “Hi Guest”):
      # Click on menu icon and then on SignIn link to go to login page
      self.click_element(self.main_menu_icon)
      self.click_element(self.signin_link)
      self.wait(2)

      self.set_text(self.login_email,username)
      self.set_text(self.login_password,password)
      self.click_element(self.submit_button)
      self.wait(3)
      login_success = self.check_login(firstname)
      if login_success:
      self.write(“Login Success for user %s”%firstname)
      else:
      self.write(“Login failed for user %s”%firstname)
      else:
      self.write(“User is already logged in. Logout and try again”)

      1. Ah I see.. Then I will use the one you wrote as a template and add more element..

        By the way, does this method allows me to test two devices in parallel (running at the same time?)
        If not, do you know any link that might help me to achieve this?

        Thank You again Avinash 🙂

      2. This method will allow you to run tests in different devices working on different versions of android.
        Probably you can refer to appium documentation to solve your issue in case you haven’t tried it already.

        Regards
        Avinash

  2. Hi Avinash

    I am trying to run multiple devices parellely. The information which u gave is really useful for me as i am newly doing automation using Appium.
    The Below Statement
    usage = “usage: %prog -c -t \nE.g.1: %prog -c D:\\Git_Qxf2\\qxf2\\samples\\clients\\BigBasket\\configuration.ini -t \”Android_4.2\”\n–

    Will this statement ask for Option to choose Android 4.2 or Android 4.4?
    If so it will run on 1 device at a time.
    How to run all at a time?
    Can you please help me understand the commands used in above statement.

    Thanks in advance.

    Harika.L.R

  3. Hi Avinash,
    I wanted to use the configuration file so that when i test my app i can load the configuration files as you suggested but i am getting error as below:
    C:\OHMGANAHSAYANAMA\Appium\Appium_scripts\Android>python OHM_Ganashayanamaha_config_main.py -c ./configuration.ini -t Android_5.1.1
    line 1
    Traceback (most recent call last):
    File “OHM_Ganashayanamaha_config_main.py”, line 35, in
    run_setup_test(config_file=options.config_file,test_run=options.test_run)
    File “OHM_Ganashayanamaha_config_main.py”, line 15, in run_setup_test
    test_obj = Android_Settings(config_file=config_file,test_run=test_run)
    File “C:\OHMGANAHSAYANAMA\Appium\Appium_scripts\Android\OHM_Ganashayanamaha_Comfiguration.py”, line 41, in __init__
    self.driver = webdriver.Remote(‘http://localhost:4723/wd/hub’, desired_caps)
    File “C:\Program Files (x86)\Python35-32\lib\site-packages\appium\webdriver\webdriver.py”, line 36, in __init__
    super(WebDriver, self).__init__(command_executor, desired_capabilities, browser_profile, proxy, keep_alive)
    File “C:\Program Files (x86)\Python35-32\lib\site-packages\selenium\webdriver\remote\webdriver.py”, line 92, in __init__
    self.start_session(desired_capabilities, browser_profile)
    File “C:\Program Files (x86)\Python35-32\lib\site-packages\selenium\webdriver\remote\webdriver.py”, line 179, in start_session
    response = self.execute(Command.NEW_SESSION, capabilities)
    File “C:\Program Files (x86)\Python35-32\lib\site-packages\selenium\webdriver\remote\webdriver.py”, line 236, in execute
    self.error_handler.check_response(response)
    File “C:\Program Files (x86)\Python35-32\lib\site-packages\selenium\webdriver\remote\errorhandler.py”, line 192, in check_response
    raise exception_class(message, screen, stacktrace)
    selenium.common.exceptions.WebDriverException: Message: A new session could not be created. (Original error: Activity used to start app doesn’t exist or cannot be launched! Make sure it exists and is a launchable activity)

    i am applying your idea to my application.
    I have four files as follows
    def run_setup_test(config_file,test_run):

    # Login to BigBasket using valid credentials
    print(“line 1”)
    test_obj = Android_Settings(config_file=config_file,test_run=test_run)
    print(“line 2”)

    suite = unittest.TestLoader().loadTestsFromTestCase(test_obj)
    unittest.TextTestRunner(verbosity=2).run(suite)

    #—START OF SCRIPT
    if __name__==’__main__’:
    #Accept some command line options from the user
    #Python module optparse
    usage = “usage: %prog -c -t \nE.g.1: %prog -c $ Path of configuration.ini -t \”Android_4.2\”\n—”
    parser = OptionParser(usage=usage)
    parser.add_option(“-c”,”–config”,dest=”config_file”,help=”The full path of the configuration file”)
    parser.add_option(“-t”,”–test_run”,dest=”test_run”,help=”The name of the test run”)
    (options,args) = parser.parse_args()

    #Create a test obj with parameters
    run_setup_test(config_file=options.config_file,test_run=options.test_run)

    Can you let me what i am doing wrong here please?

    1. Hi Selva,

      As per the logs the issue seems to be more related to Activity name you are using

      selenium.common.exceptions.WebDriverException: Message: A new session could not be created. (Original error: Activity used to start app doesn’t exist or cannot be launched! Make sure it exists and is a launchable activity)

      Can you double check the activity name you are using in the conf file

    1. Nagesh,

      The ugly way will be to wrap around the command you use to start Appium using Python’s subprocess. The command will be some form of ‘node appium’ with some additional command line parameters. Stopping the server will require killing your process.

      A nice way will be create a service and then start or the stop the service via your command line. Let us know how it goes – this is a problem I am interested in hearing about.

Leave a Reply

Your email address will not be published. Required fields are marked *