Skip to main content

One post tagged with "Java"

View All Tags

· 4 min read
Etienne DEGUINE

Light on dichroic cubes

Intro​

In this post we will show you how to set up your automation for iOS (XCUITest) and Android (uiAutomator2, Espresso) with Appium Java.

Scenario​

We want to perform basic UI testing on mobile apps.

General idea​

To automate an app, we will use Appium, which is inspired by Selenium and provides the necessary capabilities to automate. Each framework requires a set of capabtilities to be passed to the driver, this is typically the painful step to setup, which is why we give you here an overview.

iOS - XCUITest

The default automation for iOS is called XCUITest (XCode UI test). You need to have access to the build output in the form of a .app file or .ipa file to automate on iOS. Overall it's super easy and did not give us any issues.

import io.appium.java_client.MobileElement;
import io.appium.java_client.ios.IOSDriver;
import org.openqa.selenium.remote.DesiredCapabilities;
import ai.devtools.appium.SmartDriver;

DesiredCapabilities capabilities = new DesiredCapabilities();
capabilities.setCapability("app", new File("/Users/etienne/apps/SampleApp.app").getAbsolutePath());
capabilities.setCapability("allowTestPackages", true);
capabilities.setCapability("appWaitForLaunch", false);
capabilities.setCapability("newCommandTimeout", 0);
capabilities.setCapability("automationName", "XCUITest");
capabilities.setCapability("platformName", "iOS");
capabilities.setCapability("platformVersion", "14.4");
capabilities.setCapability("deviceName", "iPhone 12 Pro Max");

IOSDriver<MobileElement> androidDriver = new IOSDriver<MobileElement>(new URL("http://localhost:4723/wd/hub"), capabilities);
SmartDriver<MobileElement> smartDriver = new SmartDriver<MobileElement>(androidDriver, "<<get your api key at dev-tools.ai>>");

MobileElement element = smartDriver.findByAI("appium_username_field");
element.click();
element.sendKeys("etienne");

Android - UiAutomator2

This one is similar to iOS, it's pretty straightforward.

import io.appium.java_client.MobileElement;
import io.appium.java_client.android.AndroidDriver;
import org.openqa.selenium.remote.DesiredCapabilities;
import ai.devtools.appium.SmartDriver;

DesiredCapabilities capabilities = new DesiredCapabilities();
capabilities.setCapability("app", new File("/Users/etienne/apks/todoist.apk").getAbsolutePath());
capabilities.setCapability("allowTestPackages", true);
capabilities.setCapability("appWaitForLaunch", false);
capabilities.setCapability("newCommandTimeout", 0);

AndroidDriver<MobileElement> androidDriver = new AndroidDriver<MobileElement>(new URL("http://localhost:4723/wd/hub"), capabilities);
SmartDriver<MobileElement> smartDriver = new SmartDriver<MobileElement>(androidDriver, "<<get your api key at dev-tools.ai>>");

MobileElement element = smartDriver.findByAI("todoist_username");
element.click();
element.sendKeys("etienne");

Android - Espresso

This is where things get tricky. We noticed that there is a lot of fine grained details to setup so that Espresso can build and run properly on Android. Here is what we ended up with.

DesiredCapabilities capabilities = new DesiredCapabilities();
capabilities.setCapability("app", new File("/Users/etienne/apks/app-release.apk").getAbsolutePath());
capabilities.setCapability("allowTestPackages", true);
capabilities.setCapability("appWaitForLaunch", false);
capabilities.setCapability("newCommandTimeout", 0);
capabilities.setCapability("automationName", "Espresso");
capabilities.setCapability("platformName", "Android");
capabilities.setCapability("platformVersion", "9");
capabilities.setCapability("appium:remoteAdbHost", "0.0.0.0");
capabilities.setCapability("appium:host", "0.0.0.0");
capabilities.setCapability("appium:useKeystore", true);
capabilities.setCapability("appium:keystorePath", "/Users/etienne/Documents/old_format_keystore.keystore");
capabilities.setCapability("appium:keystorePassword", "test");
capabilities.setCapability("appium:keyAlias", "key0");
capabilities.setCapability("appium:keyPassword", "test");
capabilities.setCapability("forceEspressoRebuild", true);
capabilities.setCapability("udid", "emulator-5554");
capabilities.setCapability("noReset", false);
capabilities.setCapability("espressoBuildConfig", "{ \"additionalAppDependencies\": [ \"androidx.lifecycle:lifecycle-extensions:2.2.0\" ] }");

Let's take a look at what is going on

IPv6 confusion in Appium​

capabilities.setCapability("appium:remoteAdbHost", "0.0.0.0");
capabilities.setCapability("appium:host", "0.0.0.0");

In the Appium version we used (1.22), Appium insisted on converting localhost to ipv6 (:::1) instead of 0.0.0.0, so we have to specify it explicitly. This issue only came up for Espresso in our experimentation.

Signing the APK​

capabilities.setCapability("appium:useKeystore", true);
capabilities.setCapability("appium:keystorePath", "/Users/etienne/Documents/old_format_keystore.keystore");
capabilities.setCapability("appium:keystorePassword", "test");
capabilities.setCapability("appium:keyAlias", "key0");
capabilities.setCapability("appium:keyPassword", "test");

Due to the way it's built and run, Espresso needs to be signed with a keystore. Furthermore we had issues using a modern version of java so we had to use adoptopenjdk-8.jdk (Java 1.8) and use a keystore in the old java format.

Creating the keystore​

This is a standard java command to create the keystore. You need to make sure Android Studio uses the same keystore for signing your APK with release keys.

export JAVA_HOME=/Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home/
$JAVA_HOME/bin/keytool -genkey -v -keystore ~/Documents/old_format_keystore.keystore -alias key0 -keyalg RSA -keysize 2048 -validity 10000

Building Espresso​

Espresso requires a lot of dependencies and build options to be successful. We zeroed in on a configuration that worked. It might not be optimal but at least it works!

In terms of driver caps we need to specify the dependencies in the espressoBuildConfig

capabilities.setCapability("forceEspressoRebuild", true);
capabilities.setCapability("espressoBuildConfig", "{ \"additionalAppDependencies\": [ \"androidx.lifecycle:lifecycle-extensions:2.2.0\" ] }");

And in Android Studio in the app's build.gradle

dependencies {
implementation 'androidx.appcompat:appcompat:1.5.0'
implementation 'com.google.android.material:material:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
testImplementation 'junit:junit:4.13.2'
implementation 'androidx.test.ext:junit:1.1.3'
implementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation 'androidx.test:runner:1.4.0'
implementation 'androidx.test:rules:1.4.0'
implementation "androidx.startup:startup-runtime:1.0.0"
def lifecycle_version = "2.6.0-alpha01"
def arch_version = "2.1.0"

implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
// Lifecycles only (without ViewModel or LiveData)
implementation "androidx.lifecycle:lifecycle-runtime:$lifecycle_version"

// Saved state module for ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"

// Annotation processor
annotationProcessor "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
// alternately - if using Java8, use the following instead of lifecycle-compiler
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"

// optional - helpers for implementing LifecycleOwner in a Service
implementation "androidx.lifecycle:lifecycle-service:$lifecycle_version"

// optional - ProcessLifecycleOwner provides a lifecycle for the whole application process
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"

// optional - ReactiveStreams support for LiveData
implementation "androidx.lifecycle:lifecycle-reactivestreams:$lifecycle_version"

// optional - Test helpers for LiveData
testImplementation "androidx.arch.core:core-testing:$arch_version"

// optional - Test helpers for Lifecycle runtime
testImplementation "androidx.lifecycle:lifecycle-runtime-testing:$lifecycle_version"
}