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
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) |
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!
I am a dedicated quality assurance professional with a true passion for ensuring product quality and driving efficient testing processes. Throughout my career, I have gained extensive expertise in various testing domains, showcasing my versatility in testing diverse applications such as CRM, Web, Mobile, Database, and Machine Learning-based applications. What sets me apart is my ability to develop robust test scripts, ensure comprehensive test coverage, and efficiently report defects. With experience in managing teams and leading testing-related activities, I foster collaboration and drive efficiency within projects. Proficient in tools like Selenium, Appium, Mechanize, Requests, Postman, Runscope, Gatling, Locust, Jenkins, CircleCI, Docker, and Grafana, I stay up-to-date with the latest advancements in the field to deliver exceptional software products. Outside of work, I find joy and inspiration in sports, maintaining a balanced lifestyle.
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
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”)
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 🙂
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
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
Hi Harika,
The above statement explains which configuration you have to choose. Its either Android_4.2 or Android_4.4. If you are planning to run tests on multiple device i would suggest you use the cloud services like sauce labs.
You can also check this link below in case you haven’t looked at it already
http://stackoverflow.com/questions/18719755/multi-devices-support-in-android
Thanks & Regards
Avinash Shetty
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?
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
Hi,
Is there any way to start and stop appium programatically?
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.