This is the second in a 2-part series on using real iOS devices with Appium. It's a tutorial on getting started from scratch, authored by Appium contributor Jonah Stiennon. Assuming you've gone through all the setup instructions in the first part of this guide, we'll now be able to put it all together in the form of actual Appium scripts.
You don't need to understand how Appium controls iOS devices in order to run and write your tests, but I haven't found a written explanation of this anywhere else, so am including it for those who are curious. To get started testing right away, skip to the next section, but if you're wondering why we've had to install some of the things we need, read on.
User Interface Testing of any device relies upon the ability to launch an app and have another program inspect and interact with what the app displays on the screen. iOS is very strict about security and works hard to prevent one app from looking at what is going on in another app. In order to provide this necessary feature for testing apps, Apple built the XCUITest framework. The way it works is that Xcode has the ability to build a special app called an "XCUITest-Runner" app. The XCUITest-Runner app has access to a special set of system functions which can look at the user interface elements of another app and interact with them. These special functions are called the XCUITest API, and sparse documentation for them can be found here. (For the rest of this explanation, we'll refer to the app we are trying to test as the "app under test" or AUT, and the XCUITest-Runner app as the "runner app".)
When running tests, Xcode installs both the AUT and the runner app on the device. The runner app is a special package which includes the actual tests we wrote, and the name of the AUT. Xcode tells the device to launch the runner app, and the runner app launches the AUT. From this point forward, both the runner app and the AUT are active on the device. The runner app is invisible and works in the background, while the AUT is displayed on screen.
After the AUT launches, the runner app goes through its list of tests and runs each test, looking at the user interface of the AUT and tapping, swiping, typing into it, etc... It does this using the special XCUITest API functions.
UI tests are usually compiled into the runner app which is loaded onto the device, but Appium needs to be able to control the device as you send commands to it, rather than following a predetermined script. What we need is a test which can become any test, rather than following a prescribed set of user actions. Some bright minds at Facebook came up with a way to do this and published a special test called WebDriverAgent (or WDA). Essentially, WDA is a runner app which opens a connection to the outside world and waits for commands to be sent to it, calling the relevant XCUITest API methods for each command. The creators of WDA chose to use the eponymous WebDriver protocol for the format of these commands, the same protocol that Appium already uses for its test commands.
When your Appium tests run, they are using an Appium client to send WebDriver commands to the Appium server. The Appium server installs both your app and WDA on the iOS device, waits for WDA to start, then forwards your test commands to WDA. WDA in turn executes XCUITest API functions on the device, corresponding to the commands it receives from the Appium server. In this way, we are able to arbitrarily interact with the user interface of an iOS device.
Whenever you see a reference to WDA
in the Appium logs, this is referring to WebDriverAgent running on the device.
We have the app running on our device, now let's write a simple automated test which will launch our app and look for a particular set of words on the screen. Because all of our Apple setup has been done (hopefully) correctly, all that we need to do from the Appium side of things is use the right set of capabilities:
DesiredCapabilities capabilities = new DesiredCapabilities();
capabilities.setCapability("platformName", "iOS");
capabilities.setCapability("platformVersion", "12.0.1");
capabilities.setCapability("deviceName", "iPhone 8");
capabilities.setCapability("udid", "auto");
capabilities.setCapability("bundleId", "<your bundle id>");
capabilities.setCapability("xcodeOrgId", "<your org id>");
capabilities.setCapability("xcodeSigningId", "iPhone Developer");
capabilities.setCapability("updatedWDABundleId", "<bundle id in scope of provisioning profile>");
The trick here is knowing how to fill all of these out!
platformName
is iOS
(as you would no doubt expect)platformVersion
is the version of iOS our app is running, 12.0.1
in my case.deviceName
does not actually matter for us, since we have plugged in a real device and will select that device using the udid
desired capability. Appium still requires us to supply a value for deviceName
, though, so I put iPhone 8
.udid
is the unique ID of the device we want to run our test on. We could find our device udid by running the command instruments -s devices
in the terminal, but since we only have a single device plugged in, we can put auto
and Appium will automatically find the udid of the device for us and use it.bundleId
is the special iOS-internal name of our app, which is set in the same app-settings form where we selected our Team in Xcode. It is a unique way of identifying any app. In my case it is land.stiennon.jonah-test-app
, but you should put in a value that is correct for your app..xcodeOrgId
is the "Organizational Unit" value we made a note of earlier. It is the ID of the Developer Team which signed the certificate used to create the app.xcodeSigningId
is the first part of the "Common Name" associated with the developer certificate. Since Xcode set this up for us, it is almost always iPhone Developer
but could be something different for you if you are automating a different iOS device.updatedWDABundleId
is used by Appium to trick your device into allowing Appium to install WDA on it. You might have wondered, given how many hoops you had to jump through to get your app running on a device, how Appium is able to get WDA on the device. The short answer is that, typically, it can't. WDA's bundle ID (com.facebook.webdriveragent
) will not show up as an App ID in any of your provisioning profiles, so the app would not be allowed to run on your device. But given that Appium has the WDA code, it can actually change the bundle ID on the fly, so that when WDA is built and signed it will be allowed past Apple's security restrictions. What this means is that you must supply a bundle ID value that is allowed by an App ID in your provisioning profile. We typically recommend using wildcard App IDs (like com.test.*
), so that we can give a new bundle ID to WDA of com.test.webdriveragent
. You could also give it the same bundle ID as your app, but that could cause some confusion in the system later on. If you prefer, you can omit this capability and simply open up the WDA project inside of Appium, and make all these modifications yourself using Xcode.Your actual test code, of course, is up to you to define! You'll simply instantiate a Driver and run your test as in any other case. All the heavy lifting is done by Appium in response to the capabilities above and in the context of correctly-signed apps and correctly-provisioned devices. Here's a step-by-step guide of what to do:
Note that the bundleId
capability can only be used for apps which are already installed on our iOS device. We installed our app manually using Xcode, so the app is already there. If we make changes to the app code, we will have to click the ▶
button in Xcode in order to install the latest version of our code on the device. Then we can run our Appium tests again.
Alternatively, we could use the app
capability and set it to the path of an .ipa
file on disk. This must be an app archive generated in Xcode and signed correctly.
As a full (not working) example, see below (or click through to the code sample on GitHub). It's not working because of course you'll need to supply the correct values for your app. But you can use it as a template.
import io.appium.java_client.ios.IOSDriver;
import java.net.MalformedURLException;
import java.net.URL;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.remote.DesiredCapabilities;
public class Edition041_iOS_Real_Device {
private IOSDriver driver;
@Before
public void setUp() throws MalformedURLException {
DesiredCapabilities capabilities = new DesiredCapabilities();
capabilities.setCapability("platformName", "iOS");
capabilities.setCapability("platformVersion", "12.0.1");
capabilities.setCapability("deviceName", "iPhone 8");
capabilities.setCapability("udid", "auto");
capabilities.setCapability("bundleId", "<your bundle id>");
capabilities.setCapability("xcodeOrgId", "<your org id>");
capabilities.setCapability("xcodeSigningId", "iPhone Developer");
capabilities.setCapability("updatedWDABundleId", "<bundle id in scope of provisioning profile>");
driver = new IOSDriver<>(new URL("http://localhost:4723/wd/hub"), capabilities);
}
@After
public void tearDown() {
if (driver != null) {
driver.quit();
}
}
@Test
public void testFindingAnElement() {
driver.findElementByAccessibilityId("Login Screen");
}
}