Get Set Test an iOS app using Appium and Python

It has been more than a year since I wrote my first blog on mobile automation using Appium for Android applications. There were many requests to come up with a similar blog for iOS applications. But due to time constraints I couldn’t get to it. But as they say, better late than never! I finally have a guide to help you get started with iOS automation using Appium. By the end of this post you should be able to set up your Mac with Appium and run a test for an iOS app using a simulator.


Setup

Here are the steps to set up Xcode and Appium on Mac OS

Step1. Download Xcode
Xcode is an integrated development environment (IDE) containing a suite of software development tools developed by Apple for developing software for OS X and iOS. You need to download xcode and install it so that you can download the code for an iOS app and run it on a simulator.

Step2. Start an iOS emulator using Xcode
The Simulator app, available within Xcode, presents the iPhone, iPad or Apple Watch user interface in a window on your Mac computer. To launch simulator
1. Launch Xcode.
2. Do one of the following:

  • Choose Xcode > Open Developer Tool > Simulator.
  • Control-click the Xcode icon in the Dock, and from the shortcut menu, choose Open Developer Tool > Simulator.
  • iOS Simulator

    Note: You can also add simulators for any specific combination you want to test. For more details refer to this link

    Step3. Download Appium client
    Download the appium client. I downloaded version 1.4.13.

    Step4. Install Python and Python client Library for Appium
    There are several client libraries (in Java, Ruby, Python, PHP, JavaScript, and C#) which support Appium’s extensions to the WebDriver protocol. When using Appium, you want to use these client libraries instead of your regular WebDriver client. I have used the Appium Python client available here. Assuming you have pip installed on your machine, you can use the following command to install it

    pip install Appium-Python-Client

    Step5. Start the Appium server console
    Just double click on Appium file to start the appium server console. Then click on Launch button to start the Appium node server

    Appium


    Your first Test using Appium

    For this test we will use a Table Search with the UISearchController app. We picked the UISearchController app because its source code was available to everyone and we could show you how to build the app and run it on a simulator. UISearchController is an iOS sample application that demonstrates how to use UISearchController.

    Step1. Download the app to test
    You can download the source code of Table Search with UISearchController app from here. The code is available in both swift and Objective-C language. We will be using the Swift code as it is a new programming language for iOS, OS X, watchOS, and tvOS apps that builds on the best of C and Objective-C.

    Step2. Open the project using Xcode
    Launch Xcode. Click on Open another project and select TableSearch-swift.xcodeproj inside the Swift folder to open the project
    Open Project Xcode

    Step3. Build the app and run it using a simulator
    Once you have the source code you can build the app and run it on the simulator. Just click on the “Build and then run the current scheme” button (Play icon near top left). Once the build completes you should get a build succeeded message and the app should open in the simulator selected.

    X-Code

    Simulator

    Step4. Get the .app file and bundle id
    To write your script, you will need the path of the .app file as well as a bundle id.

    In case if you have already built and run the app on your simulator you may not need the .app file to run the test on the same simulator. Else you need the Search.swift.app file to run the test.

    If you have run the app on a simulator you can find the .app file in below path

    ~/Library/Developer/CoreSimulator/Devices/{{Device Code}}/data/Containers/Bundle/Application

    To get the bundle id, right click on the Search.swift.app file and do a Show Package Contents to view the info.plist file.
    Info.plist

    Step5. Get the xpath of elements using Appium Inspector
    Our test will search for an Apple device and click on it to view its details and then navigate back to the home screen of the application.

    For this we need to find the xpath of different elements like Search field. We will use Appium Inspector for this.
    Open the iOS setting in Appium, enter the App path of your test application and check the box next to it. Give the device information like device name and platform version. Now click on the Inspector icon at the top as shown in the screenshot below

    Note: Uncheck the BundelID field as i had issues when trying to launch appium Inspector with it populated
    Appium_Inspector

    The Appium Inspector will get launched. Now you can find any element and it’s name by either clicking the element on the preview page provided, or locating it in the UI navigator. In this test, I’m looking for the details of Search box field.

    Appium_Inspector_Search

    Now we can use the details to build my xpath for Search field. Similarly you can use the appium Inspector to view the details of any field in the App.

    Xpath of Search field = “//UIASearchBar[@name=’Search’]”

    Step6. Write the test

    Create a test script (Table_Search.py) in $Directory_Of_My_Choice based on the snippet below. I have my Search.swift.app file inside the ‘TableSearchwithUISearchController/Swift’ folder within my $Directoy_Of_My_Choice. I have hard coded the path in this example but you can use a combination of os.path.abspath and os.path.dirname(__file__) to make it a path relative to your test file.

    Also pay particular attention to the setup() method. You will run into all sorts of cryptic errors if the platformVersion, deviceName and bundleId are not set correctly.

     
    """
    Simple iOS tests for table search app.
    NOTE: This is throwaway code to help testers get started with iOS automation
    """
    import unittest
    import os
    from appium import webdriver
    from time import sleep
     
    class TableSearchTest(unittest.TestCase):
     
        def setUp(self):
            # Set up appium
            app = os.path.join(os.path.dirname(__file__),
                               'TableSearchwithUISearchController/Swift',
                               'Search.swift.app')
            app = os.path.abspath(app)
            self.driver = webdriver.Remote(
                command_executor='http://127.0.0.1:4723/wd/hub',
                desired_capabilities={
                    'app': app,
                    'platformName': 'iOS',
                    'platformVersion': '9.2',
                    'deviceName': 'iPhone 5s',
                    'bundleId':'com.example.apple-samplecode.Search-swift'
                })
     
     
        def test_search_field(self):
            # Search for an Apple device and click on it to view the details and navigate back
            # Find the search element and perform send keys action
            search_element = self.driver.find_element_by_xpath("//UIASearchBar[@name='Search']")
            search_element.send_keys("iPad")
            sleep(2)
            # Get the xpath of first element
            first_element = self.driver.find_element_by_xpath("//UIAApplication[1]/UIAWindow[1]/UIATableView[1]/UIATableCell[1]/UIAStaticText[1]")
            # Assert that the text matches
            self.assertEqual('iPad', first_element.get_attribute('name'))
            # Perform click action
            first_element.click()
            sleep(2)
            # Click on search element
            self.driver.find_element_by_name("Search").click()
     
     
        def tearDown(self):
            self.driver.quit()
     
     
    if __name__ == '__main__':
        suite = unittest.TestLoader().loadTestsFromTestCase(TableSearchTest)
        unittest.TextTestRunner(verbosity=2).run(suite)

    Step7. Run the test and check result
    Run the script Table_Search.py file. The Table Search application is launched in the simulator and test runs.
    Run Test


    There you have it – a tutorial to get you started with iOS automation using Appium and Python.


    Subscribe to our weekly Newsletter


    View a sample



    14 thoughts on “Get Set Test an iOS app using Appium and Python

    1. Hi Avinash Sir,

      Kindly help in below question
      How do i launch Chrome browser in Appium Python Language ?

      Regards
      Raj

        1. I tried this solution with iOS 10.3 and I failed to set it up. I got error:
          [XCUITest] Error: Could not find app at ‘~/Library/Developer/CoreSimulator/Devices/1D37B64A-35B3-4964-AE78-6292ECC35B7F/data/Containers/Bundle/Application/D8373724-B232-4C9B-96C2-A5D19FD4A4B4/Search.swift.app’ which I was not able to resolve

        2. Hi,
          Can you cross check the path where you have the Search.swift.app file. Is it inside “~/Library/Developer/CoreSimulator/Devices/1D37B64A-35B3-4964-AE78-6292ECC35B7F/data/Containers/Bundle/Application/D8373724-B232-4C9B-96C2-A5D19FD4A4B4/” folder

          Thanks & Regards
          Avinash Shetty

    2. Hi Avinash,

      Great information. Thanks for sharing. Would like to know if Appium will support all versions of Swift, how developed is the appium framework with Swift vs Objective C and how fast is appium when compared to xctest.

      Thanks
      PP

      1. Hi,
        I have tried iOS apps developed using Swift and it worked fine for me. A bit of research also showed me that there’s also a swift selenium client: https://github.com/appium/selenium-swift, which I haven’t tried still. Also after deprecation of UIAutomation in Xcode, there is an Appium iOS driver, backed by Apple XCUITest (https://github.com/appium/appium-xcuitest-driver).
        I haven’t used XCTest so I really can’t tell which is faster between appium and XCTest, but I feel XCTest would be faster than many external frameworks.

    3. Appium server is up and running
      I’ve changed localhost:4723 to 127.0.0.1:4723
      app = os.path.join(os.path.dirname(__file__),
      ‘/Users/project.deal/Desktop/Gauge.app’)
      Appium works well but i tried to run on pycharm , there is no log on appium and it returns this error =>

      BasicTest.py:19:
      _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
      ../../Library/Python/2.7/lib/python/site-packages/appium/webdriver/webdriver.py:37: in __init__
      super(WebDriver, self).__init__(command_executor, desired_capabilities, browser_profile, proxy, keep_alive)
      /usr/local/lib/python2.7/site-packages/selenium/webdriver/remote/webdriver.py:98: in __init__
      self.start_session(desired_capabilities, browser_profile)
      /usr/local/lib/python2.7/site-packages/selenium/webdriver/remote/webdriver.py:188: in start_session
      response = self.execute(Command.NEW_SESSION, parameters)
      /usr/local/lib/python2.7/site-packages/selenium/webdriver/remote/webdriver.py:252: in execute
      self.error_handler.check_response(response)
      _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

      self =
      response = {‘status’: 500, ‘value’: ”}

      def check_response(self, response):
      “””
      Checks that a JSON response from the WebDriver does not have an error.

      :Args:
      – response – The JSON response from the WebDriver server as a dictionary
      object.

      :Raises: If the response contains an error message.
      “””
      status = response.get(‘status’, None)
      if status is None or status == ErrorCode.SUCCESS:
      return
      value = None
      message = response.get(“message”, “”)
      screen = response.get(“screen”, “”)
      stacktrace = None
      if isinstance(status, int):
      value_json = response.get(‘value’, None)
      if value_json and isinstance(value_json, basestring):
      import json
      try:
      value = json.loads(value_json)
      if len(value.keys()) == 1:
      value = value[‘value’]
      status = value.get(‘error’, None)
      if status is None:
      status = value[“status”]
      message = value[“value”]
      if not isinstance(message, basestring):
      value = message
      message = message.get(‘message’)
      else:
      message = value.get(‘message’, None)
      except ValueError:
      pass

      exception_class = ErrorInResponseException
      if status in ErrorCode.NO_SUCH_ELEMENT:
      exception_class = NoSuchElementException
      elif status in ErrorCode.NO_SUCH_FRAME:
      exception_class = NoSuchFrameException
      elif status in ErrorCode.NO_SUCH_WINDOW:
      exception_class = NoSuchWindowException
      elif status in ErrorCode.STALE_ELEMENT_REFERENCE:
      exception_class = StaleElementReferenceException
      elif status in ErrorCode.ELEMENT_NOT_VISIBLE:
      exception_class = ElementNotVisibleException
      elif status in ErrorCode.INVALID_ELEMENT_STATE:
      exception_class = InvalidElementStateException
      elif status in ErrorCode.INVALID_SELECTOR \
      or status in ErrorCode.INVALID_XPATH_SELECTOR \
      or status in ErrorCode.INVALID_XPATH_SELECTOR_RETURN_TYPER:
      exception_class = InvalidSelectorException
      elif status in ErrorCode.ELEMENT_IS_NOT_SELECTABLE:
      exception_class = ElementNotSelectableException
      elif status in ErrorCode.ELEMENT_NOT_INTERACTABLE:
      exception_class = ElementNotInteractableException
      elif status in ErrorCode.INVALID_COOKIE_DOMAIN:
      exception_class = WebDriverException
      elif status in ErrorCode.UNABLE_TO_SET_COOKIE:
      exception_class = WebDriverException
      elif status in ErrorCode.TIMEOUT:
      exception_class = TimeoutException
      elif status in ErrorCode.SCRIPT_TIMEOUT:
      exception_class = TimeoutException
      elif status in ErrorCode.UNKNOWN_ERROR:
      exception_class = WebDriverException
      elif status in ErrorCode.UNEXPECTED_ALERT_OPEN:
      exception_class = UnexpectedAlertPresentException
      elif status in ErrorCode.NO_ALERT_OPEN:
      exception_class = NoAlertPresentException
      elif status in ErrorCode.IME_NOT_AVAILABLE:
      exception_class = ImeNotAvailableException
      elif status in ErrorCode.IME_ENGINE_ACTIVATION_FAILED:
      exception_class = ImeActivationFailedException
      elif status in ErrorCode.MOVE_TARGET_OUT_OF_BOUNDS:
      exception_class = MoveTargetOutOfBoundsException
      else:
      exception_class = WebDriverException
      if value == ” or value is None:
      value = response[‘value’]
      if isinstance(value, basestring):
      if exception_class == ErrorInResponseException:
      raise exception_class(response, value)
      > raise exception_class(value)
      E WebDriverException: Message:

      /usr/local/lib/python2.7/site-packages/selenium/webdriver/remote/errorhandler.py:165: WebDriverException

      =================================== FAILURES ===================================
      _________________________ BasicTest.test_search_field __________________________

      self =

      def setUp(self):
      # Set up appium
      app = os.path.join(os.path.dirname(__file__),
      ‘/Users/muge.karakas/Desktop/Bilyoner.app’)
      app = os.path.abspath(app)
      self.driver = webdriver.Remote(
      command_executor=’http://localhost:4723/wd/hub’,
      desired_capabilities={
      ‘app’: app,
      ‘platformName’: ‘iOS’,
      ‘platformVersion’: ‘9.3’,
      > ‘deviceName’: ‘yyy’
      })

      BasicTest.py:19:
      _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
      ../../Library/Python/2.7/lib/python/site-packages/appium/webdriver/webdriver.py:37: in __init__
      super(WebDriver, self).__init__(command_executor, desired_capabilities, browser_profile, proxy, keep_alive)
      /usr/local/lib/python2.7/site-packages/selenium/webdriver/remote/webdriver.py:98: in __init__
      self.start_session(desired_capabilities, browser_profile)
      /usr/local/lib/python2.7/site-packages/selenium/webdriver/remote/webdriver.py:188: in start_session
      response = self.execute(Command.NEW_SESSION, parameters)
      /usr/local/lib/python2.7/site-packages/selenium/webdriver/remote/webdriver.py:252: in execute
      self.error_handler.check_response(response)
      _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

      self =
      response = {‘status’: 500, ‘value’: ”}

      def check_response(self, response):
      “””
      Checks that a JSON response from the WebDriver does not have an error.

      :Args:
      – response – The JSON response from the WebDriver server as a dictionary
      object.

      :Raises: If the response contains an error message.
      “””
      status = response.get(‘status’, None)
      if status is None or status == ErrorCode.SUCCESS:
      return
      value = None
      message = response.get(“message”, “”)
      screen = response.get(“screen”, “”)
      stacktrace = None
      if isinstance(status, int):
      value_json = response.get(‘value’, None)
      if value_json and isinstance(value_json, basestring):
      import json
      try:
      value = json.loads(value_json)
      if len(value.keys()) == 1:
      value = value[‘value’]
      status = value.get(‘error’, None)
      if status is None:
      status = value[“status”]
      message = value[“value”]
      if not isinstance(message, basestring):
      value = message
      message = message.get(‘message’)
      else:
      message = value.get(‘message’, None)
      except ValueError:
      pass

      exception_class = ErrorInResponseException
      if status in ErrorCode.NO_SUCH_ELEMENT:
      exception_class = NoSuchElementException
      elif status in ErrorCode.NO_SUCH_FRAME:
      exception_class = NoSuchFrameException
      elif status in ErrorCode.NO_SUCH_WINDOW:
      exception_class = NoSuchWindowException
      elif status in ErrorCode.STALE_ELEMENT_REFERENCE:
      exception_class = StaleElementReferenceException
      elif status in ErrorCode.ELEMENT_NOT_VISIBLE:
      exception_class = ElementNotVisibleException
      elif status in ErrorCode.INVALID_ELEMENT_STATE:
      exception_class = InvalidElementStateException
      elif status in ErrorCode.INVALID_SELECTOR \
      or status in ErrorCode.INVALID_XPATH_SELECTOR \
      or status in ErrorCode.INVALID_XPATH_SELECTOR_RETURN_TYPER:
      exception_class = InvalidSelectorException
      elif status in ErrorCode.ELEMENT_IS_NOT_SELECTABLE:
      exception_class = ElementNotSelectableException
      elif status in ErrorCode.ELEMENT_NOT_INTERACTABLE:
      exception_class = ElementNotInteractableException
      elif status in ErrorCode.INVALID_COOKIE_DOMAIN:
      exception_class = WebDriverException
      elif status in ErrorCode.UNABLE_TO_SET_COOKIE:
      exception_class = WebDriverException
      elif status in ErrorCode.TIMEOUT:
      exception_class = TimeoutException
      elif status in ErrorCode.SCRIPT_TIMEOUT:
      exception_class = TimeoutException
      elif status in ErrorCode.UNKNOWN_ERROR:
      exception_class = WebDriverException
      elif status in ErrorCode.UNEXPECTED_ALERT_OPEN:
      exception_class = UnexpectedAlertPresentException
      elif status in ErrorCode.NO_ALERT_OPEN:
      exception_class = NoAlertPresentException
      elif status in ErrorCode.IME_NOT_AVAILABLE:
      exception_class = ImeNotAvailableException
      elif status in ErrorCode.IME_ENGINE_ACTIVATION_FAILED:
      exception_class = ImeActivationFailedException
      elif status in ErrorCode.MOVE_TARGET_OUT_OF_BOUNDS:
      exception_class = MoveTargetOutOfBoundsException
      else:
      exception_class = WebDriverException
      if value == ” or value is None:
      value = response[‘value’]
      if isinstance(value, basestring):
      if exception_class == ErrorInResponseException:
      raise exception_class(response, value)
      > raise exception_class(value)
      E WebDriverException: Message:

      /usr/local/lib/python2.7/site-packages/selenium/webdriver/remote/errorhandler.py:165: WebDriverException
      =========================== 1 failed in 0.20 seconds ===========================
      Process finished with exit code 0

      1. I think you have the path set wrong. For example. are you sure this line is correct?

        app = os.path.join(os.path.dirname(__file__),
        ‘/Users/project.deal/Desktop/Gauge.app’)
        

        What this code will do is to figure out:
        a) the directory of the Python file that has this line (e.g.: /users/mge)
        b) append ‘/Users/project.deal/Desktop/Gauge.app’ to the value in a) (So: /users/mge/Users/project.deal/Desktop/Gauge.app)

        To debug you can do one of two things:
        a) Print out the variable app and see if the path looks correct
        b) Hardcode app to the exact path of the .app file

        Both a) and b) will tell you if this piece of code is the problem.

        1. i solved this problem. Maybe others will be have this problem later. it’s selenium version issue. i’ve last version 3.4.3 then i’ve downgraded selenium version 2.53.6 errors disappear.

    4. You are right. Correct path is /Users/muge.karakas/Desktop/Bilyoner.app. i wrote wrong it here. As a result i have still problem.

    Leave a Reply

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