{"id":2569,"date":"2015-03-13T07:26:32","date_gmt":"2015-03-13T11:26:32","guid":{"rendered":"http:\/\/qxf2.com\/blog\/?p=2569"},"modified":"2018-04-23T10:34:34","modified_gmt":"2018-04-23T14:34:34","slug":"running-mobile-automation-on-multiple-devices","status":"publish","type":"post","link":"https:\/\/qxf2.com\/blog\/running-mobile-automation-on-multiple-devices\/","title":{"rendered":"Python Appium tests on different Android versions"},"content":{"rendered":"<p><strong>Problem:<\/strong> UI element identifiers are not consistent across mobile platforms. <\/p>\n<p><a href=\"http:\/\/ben-evans.com\/benedictevans\/2014\/10\/28\/presentation-mobile-is-eating-the-world\">Mobile is eating the world<\/a>. 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.<\/p>\n<hr>\n<h3>Why this post?<\/h3>\n<p>Qxf2&#8217;s <a href=\"http:\/\/www.qxf2.com\/goals?utm_source=blog&#038;utm_medium=click&#038;utm_campaign=From%20blog\">top goal<\/a> 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. <\/p>\n<hr>\n<h3>New to mobile automation?<\/h3>\n<p>If you are new to mobile automation, we suggest the following posts to get started:<br \/>\na) <a href=\"https:\/\/qxf2.com\/blog\/appium-mobile-automation\/\">Run Appium on an emulator<\/a><br \/>\nb) <a href=\"https:\/\/qxf2.com\/blog\/appium-tutorial-python-physical-device\/\">Run Appium on mobile devices<\/a><br \/>\nc) <a href=\"https:\/\/qxf2.com\/blog\/identify-ui-elements-mobile-apps\/\">Identify UI elements on mobile platforms<\/a><br \/>\nd) <a href=\"https:\/\/qxf2.com\/blog\/automating-pinch-zoom-swipe-appium\/\">Execute different mobile gestures<\/a><\/p>\n<hr>\n<h3>Overview<\/h3>\n<p>We will use <a href=\"http:\/\/bigbasket.com\/\">BigBasket<\/a> as the application under test over the next few posts. BigBasket is one of India\u2019s 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. <\/p>\n<p>At a high level, these are the steps we will perform<br \/>\n1. Write a &#8216;Base&#8217; class<br \/>\n2. Create a configuration file<br \/>\n3. Accept the OS version as a command line parameter<br \/>\n4. Load the identifiers you need based on OS version<br \/>\n5. Build a logic to use the right method to find the UI element<br \/>\n6. Write the test<\/p>\n<figure id=\"attachment_2616\" aria-describedby=\"caption-attachment-2616\" style=\"width: 799px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2015\/03\/mobileAppiumBlog.jpg\" data-rel=\"lightbox-image-0\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2015\/03\/mobileAppiumBlog-799x1024.jpg\" alt=\"Appium_Test\" width=\"799\" height=\"1024\" class=\"size-large wp-image-2616\" srcset=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2015\/03\/mobileAppiumBlog-799x1024.jpg 799w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2015\/03\/mobileAppiumBlog-234x300.jpg 234w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2015\/03\/mobileAppiumBlog.jpg 1888w\" sizes=\"auto, (max-width: 799px) 100vw, 799px\" \/><\/a><figcaption id=\"caption-attachment-2616\" class=\"wp-caption-text\">An approach to running mobile automation scripts on multiple devices<\/figcaption><\/figure>\n<h4>1. Write a &#8216;Base&#8217; class<\/h4>\n<p>We have chosen to use the <a href=\"http:\/\/en.wikipedia.org\/wiki\/Facade_pattern\">facade pattern<\/a> 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 <strong>BigBasket_Base_Appium.py<\/strong> and that Base has only these methods.<\/p>\n<pre lang=\"python\">\r\n\"\"\"\r\nQxf2 Base Appium script for BigBasket Android app\r\n\"\"\"\r\nimport unittest,time, sys, os, dotenv\r\nfrom appium import webdriver\r\nfrom appium.webdriver.common.touch_action import TouchAction\r\nfrom appium.webdriver.common.multi_action import MultiAction\r\nfrom optparse import OptionParser\r\nfrom ConfigParser import SafeConfigParser\r\n\r\nclass BigBasket_Base_Appium(unittest.TestCase):\r\n    \"Base class for BigBasket's Appium tests\"\r\n    def __init__(self,config_file,test_run):\r\n        \"Constructor\"\r\n        #test_run is the section name you use in the configuration file\r\n        #For example, we have used Android_4.4 and Android_4.2\r\n        self.test_run = test_run \r\n        \r\n        #More to be filled in later in the tutorial\r\n\r\n    def set_text(self,ui_identifier,value):\r\n        \"Set the value of the text field\"\r\n        flag = False\r\n        if value is not None:\r\n            text_field = self.get_element(ui_identifier)\r\n            if text_field is not None:\r\n                try:\r\n                    text_field.clear()\r\n                    text_field.send_keys(value)\r\n                    flag = True\r\n                except Exception, e:\r\n                    print 'Exception when setting the text field: %s'%ui_identifier\r\n        \r\n        return flag\r\n\r\n    def click_element(self,xpath):\r\n        \"Click the button supplied\"\r\n        flag = False\r\n        try:\r\n            link = self.get_element(xpath)\r\n            if link is not None:\r\n                link.click()\r\n                flag = True\r\n        except Exception,e:\r\n                print 'Exception when clicking element with xpath: %s'%xpath\r\n                print e\r\n        return flag\r\n\r\n    def get_element(self,path):\r\n        \"Return the DOM element for the xpath Or id or class name provided\"\r\n        #To be filled out later in the tutorial\r\n        pass\r\n<\/pre>\n<h4>2. Create a configuration file<\/h4>\n<p>Let us create a configuration file that uses &#8216;Android_4.x&#8217; to identify sections and list the desired configuration under it. For our example we created a file called <strong>configuration.ini<\/strong>. We added the contents below in the configuration file for different version of Android OS. In this example we are showing you two &#8216;test_runs&#8217; (Android_4.2 and Android_4.4) using different configurations.<\/p>\n<pre lang='python'>\r\n[Android_4.2]\r\nplatformName = Android\r\nplatformVersion = 4.2\r\ndeviceName = Samsung\r\nappPackage = com.bigbasket.mobileapp\r\nappActivity = .activity.SplashActivity\r\nuser_title = android.widget.TextView\r\nmain_menu_icon = \/\/android.widget.ImageView[@bounds='[408,59][456,107]']\r\nsignin_link = \/\/android.widget.TextView[@text='Sign In']\r\nsignout_link = \/\/android.widget.TextView[@text='Sign Out']\r\nlogin_email = \/\/android.widget.EditText[@bounds='[15,242][465,290]']\r\nlogin_password = \/\/android.widget.EditText[@bounds='[15,337][465,385]']\r\n\r\n[Android_4.4]\r\nplatformName = Android\r\nplatformVersion = 4.4\r\ndeviceName = Moto E\r\nappPackage = com.bigbasket.mobileapp\r\nappActivity = .activity.SplashActivity\r\nuser_title = android.widget.TextView\r\nmain_menu_icon = com.bigbasket.mobileapp:id\/settingImageHome\r\nsignin_link = com.bigbasket.mobileapp:id\/signin\r\nsignout_link = com.bigbasket.mobileapp:id\/signup\r\nlogin_email = com.bigbasket.mobileapp:id\/emailsig\r\nlogin_password = com.bigbasket.mobileapp:id\/pwdsig\r\n<\/pre>\n<h4>3. Accept the OS version as a command line parameter<\/h4>\n<p>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 <strong>BigBasket_Login_Appium.py<\/strong>. The code snippet to accept the configuration file and command line is given below.<\/p>\n<pre lang='python'>\r\nif __name__=='__main__':\r\n    #Accept some command line options from the user\r\n    #We used Python module optparse \r\n    usage = \"usage: %prog -c  -t  \\nE.g.1: %prog -c D:\\\\BigBasket\\\\configuration.ini -t \\\"Android_4.2\\\"\\n---\"\r\n    parser = OptionParser(usage=usage)\r\n    parser.add_option(\"-c\",\"--config\",dest=\"config_file\",help=\"The full path of the configuration file\")\r\n    parser.add_option(\"-t\",\"--test_run\",dest=\"test_run\",help=\"The name of the test run\")\r\n    (options,args) = parser.parse_args()\r\n<\/pre>\n<h4>4. Load the identifiers you need based on OS version<\/h4>\n<p>We have chosen to use the module <a href=\"http:\/\/pymotw.com\/2\/ConfigParser\/\">ConfigParser<\/a> to read our configuration file. We can modify the constructor of the Base class. Modify the __init__ method in <strong>BigBasket_Base_Appium.py<\/strong> as follows<\/p>\n<pre lang='python'>\r\n    def __init__(self,config_file,test_run):\r\n        \"Constructor\"\r\n        #test_run is the section name you use in the configuration file\r\n        #For this example, we have used Android_4.4 and Android_4.2\r\n        self.test_run = test_run \r\n\r\n        #Read the config file\r\n        self.config_file = config_file\r\n        parser = SafeConfigParser() \r\n        parser.read(self.config_file)\r\n\r\n        #Desired capabilities can be read from the config file\r\n        desired_caps = {}\r\n        desired_caps['platformName'] = parser.get(self.test_run, 'platformName')\r\n        desired_caps['platformVersion'] = parser.get(self.test_run, 'platformVersion')\r\n        desired_caps['deviceName'] = parser.get(self.test_run, 'deviceName')\r\n\r\n        # Make sure you install the BigBasket app using Play Store and then launch it directly using package and activity name\r\n        desired_caps['appPackage'] = parser.get(self.test_run, 'appPackage')\r\n        desired_caps['appActivity'] = parser.get(self.test_run, 'appActivity')\r\n        self.driver = webdriver.Remote('http:\/\/localhost:4723\/wd\/hub', desired_caps)\r\n        self.action = TouchAction(self.driver)\r\n        \r\n        #UI identifiers start here \r\n        self.user_title = parser.get(self.test_run, 'user_title')\r\n        self.main_menu_icon = parser.get(self.test_run, 'main_menu_icon')\r\n        self.signin_link = parser.get(self.test_run, 'signin_link')\r\n        self.login_email = parser.get(self.test_run, 'login_email')\r\n        self.login_password  = parser.get(self.test_run, 'login_password')\r\n        self.submit_button = parser.get(self.test_run, 'submit_button')\r\n<\/pre>\n<h4>5. Build a logic to use the right method to find the UI element<\/h4>\n<p>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 <strong>BigBasket_Base_Appium.py<\/strong> as follows<\/p>\n<pre lang='python'>\r\n    def get_element(self,path):\r\n        \"Return the DOM element for the xpath Or id or class name provided\"\r\n        dom_element = None\r\n        try:\r\n            if ('\/\/'in path):\r\n                dom_element = self.driver.find_element_by_xpath(path)\r\n            elif (':id\/'in path):\r\n                dom_element = self.driver.find_element_by_id(path)\r\n            else:\r\n                dom_element = self.driver.find_element_by_class_name(path)\r\n        except Exception,e:\r\n            print 'Exception while getting the path: %s'%path\r\n            print e\r\n        return dom_element\r\n<\/pre>\n<p>NOTE: The above simplistic check for xpath is not our favorite method but it has worked surprisingly well for us.<\/p>\n<h4>6. Write the test<\/h4>\n<p>Now you can complete <strong>BigBasket_Login_Appium.py<\/strong>. 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. <\/p>\n<pre lang=\"python\">\r\n\"\"\"\r\nQxf2 test case to login to BigBasket.\r\n\"\"\"\r\n\r\nfrom BigBasket_Base_Appium import BigBasket_Base_Appium\r\nimport os,time\r\nfrom optparse import OptionParser\r\n\r\n\r\ndef run_login_test(config_file,test_run):\r\n    \"Example test\"\r\n    USERNAME = 'YOUR USERNAME'\r\n    PASSWORD = 'YOUR PASSWORD'\r\n    FIRSTNAME = 'YOUR FIRSTNAME'\r\n\r\n    # Login to BigBasket using valid credentials\r\n    test_obj = BigBasket_Base_Appium(config_file=config_file,test_run=test_run)\r\n    test_obj.login(username=USERNAME, password=PASSWORD, firstname=FIRSTNAME)\r\n\r\n     \r\n#---START OF SCRIPT\r\nif __name__=='__main__':\r\n    #Accept some command line options from the user\r\n    #Python module optparse \r\n    usage = \"usage: %prog -c  -t  \\nE.g.1: %prog -c $ Path of configuration.ini -t \\\"Android_4.2\\\"\\n---\"\r\n    parser = OptionParser(usage=usage)\r\n    parser.add_option(\"-c\",\"--config\",dest=\"config_file\",help=\"The full path of the configuration file\")\r\n    parser.add_option(\"-t\",\"--test_run\",dest=\"test_run\",help=\"The name of the test run\")\r\n    (options,args) = parser.parse_args()\r\n \r\n    #Create a test obj with parameters\r\n    run_login_test(config_file=options.config_file,test_run=options.test_run)\r\n<\/pre>\n<p>Run the test:<br \/>\n<a href=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2015\/03\/Running_Login_Test.jpg\" data-rel=\"lightbox-image-1\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2015\/03\/Running_Login_Test-300x56.jpg\" alt=\"Running Login Test\" width=\"300\" height=\"56\" class=\"aligncenter size-medium wp-image-2601\" srcset=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2015\/03\/Running_Login_Test-300x56.jpg 300w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2015\/03\/Running_Login_Test.jpg 673w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/a><\/p>\n<hr>\n<p>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 <em>one<\/em> 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.<\/p>\n<p>As always, leave your questions and comments below!<\/p>\n<hr>\n<script>(function() {\n\twindow.mc4wp = window.mc4wp || {\n\t\tlisteners: [],\n\t\tforms: {\n\t\t\ton: function(evt, cb) {\n\t\t\t\twindow.mc4wp.listeners.push(\n\t\t\t\t\t{\n\t\t\t\t\t\tevent   : evt,\n\t\t\t\t\t\tcallback: cb\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n})();\n<\/script><!-- Mailchimp for WordPress v4.10.1 - https:\/\/wordpress.org\/plugins\/mailchimp-for-wp\/ --><form id=\"mc4wp-form-1\" class=\"mc4wp-form mc4wp-form-6165 mc4wp-form-theme mc4wp-form-theme-blue\" method=\"post\" data-id=\"6165\" data-name=\"Newsletter\" ><div class=\"mc4wp-form-fields\"><div style=\"border:3px; border-style:dashed;border-color:#56d1e1;padding:1.2em;\">\r\n  <h1 style=\"text-align: center; padding-top: 20px; padding-bottom: 20px; color: #592b1b;\">Subscribe to our weekly Newsletter<\/h1>\r\n  <input style=\"margin: auto;\" type=\"email\" name=\"EMAIL\" placeholder=\"Your email address\" required \/>\r\n  <br>\r\n  <p style=\"text-align: center;\">\r\n    <input style=\"background-color: #890c06 !important; border-color: #890c06;\" type=\"submit\" value=\"Sign up\" \/>\r\n    \r\n  <\/p>\r\n  <p style=\"text-align: center;\">\r\n    <a href=\"http:\/\/mailchi.mp\/c9c7b81ddf13\/the-informed-testers-newsletter-20-oct-2017\"><small>View a sample<\/small><\/a>\r\n  <\/p>\r\n  <br>\r\n<\/div><\/div><label style=\"display: none !important;\">Leave this field empty if you're human: <input type=\"text\" name=\"_mc4wp_honeypot\" value=\"\" tabindex=\"-1\" autocomplete=\"off\" \/><\/label><input type=\"hidden\" name=\"_mc4wp_timestamp\" value=\"1776770200\" \/><input type=\"hidden\" name=\"_mc4wp_form_id\" value=\"6165\" \/><input type=\"hidden\" name=\"_mc4wp_form_element_id\" value=\"mc4wp-form-1\" \/><div class=\"mc4wp-response\"><\/div><\/form><!-- \/ Mailchimp for WordPress Plugin -->\n<hr>\n","protected":false},"excerpt":{"rendered":"<p>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 [&hellip;]<\/p>\n","protected":false},"author":3,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[48,50,38,47,71,18,30],"tags":[],"class_list":["post-2569","post","type-post","status-publish","format-standard","hentry","category-android","category-appium","category-automation","category-mobile","category-mobile-automation","category-python","category-selenium"],"_links":{"self":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts\/2569","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/comments?post=2569"}],"version-history":[{"count":46,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts\/2569\/revisions"}],"predecessor-version":[{"id":6216,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts\/2569\/revisions\/6216"}],"wp:attachment":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/media?parent=2569"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/categories?post=2569"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/tags?post=2569"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}