Edition 71

Starting an Appium Server Programmatically Using AppiumServiceBuilder

Reader kuznietsov.com suggested that we write an article introducing the AppiumServiceBuilder functionality built into the Appium Java client.

All of our example code assumes that an Appium server is already running, and this is how many test suites start out too. It's so much more convenient if an Appium server is started automatically when the tests begin. The Java client has a convenient couple of classes for starting and stopping an Appium server. Other languages may have separate packages for doing this, or you can execute the appium command as a separate process.

Node.js has the advantage of being the language Appium is written in, so an Appium server can be started just by requireing it:

let Appium = require('appium')
let server = await Appium.main()
// server now running
await server.close()
// server stopped

Back in Java land, we start by creating an AppiumServiceBuilder which we then use to create an AppiumDriverLocalService:

AppiumServiceBuilder serviceBuilder = new AppiumServiceBuilder();
// some additional setup may be needed here, keep reading for more details
server = AppiumDriverLocalService.buildService(serviceBuilder);

Now that we have an AppiumDriverLocalService named server, we can start and stop it easily. When starting, an Appium server will run, and you will be able to connect to it. The Appium server logs, by default, are printed in the output of the test.

server.start();
// you can create connect clients and automate tests now

Don't forget to stop the server when you are done:

server.stop()

In order to run an Appium server, the Java code in the AppiumServiceBuilder needs to know the location of the Node.js executable on your computer, and also the location of the Appium package itself. It has ways to guess the location, but those did not work on my machine, since I installed Node.js using nvm. If you only have Appium Desktop installed, you will also have to install the Appium node package, so it can be run from the commandline.

AppiumServiceBuilder serviceBuilder = new AppiumServiceBuilder();
serviceBuilder.usingDriverExecutable(new File("/path/to/node/executable"));
serviceBuilder.withAppiumJS(new File("/path/to/appium"));

If you share your test code with a team and/or run in different CI environments, you can set this information via environment variables rather than hardcoding file paths in your test code. Store the paths in environment variables named NODE_PATH and APPIUM_PATH, and the AppiumServiceBuilder will pick them up automatically.

Another issue I ran into is that the XCUITest driver requires the Carthage package manager to be in the system PATH. If you followed the installation instructions for XCUITest driver, Carthage is installed using Homebrew and it is automatically added to the PATH, so this isn't normally an issue. Unfortunately, the way that AppiumDriverLocalService runs Appium, it does not re-use your defauly PATH and instead reverts to the system default. For my case, I corrected this by specifying a new PATH environment variable in the AppiumServiceBuilder:

HashMap<String, String> environment = new HashMap();
environment.put("PATH", "/usr/local/bin:" + System.getenv("PATH"));
serviceBuilder.withEnvironment(environment);

You can add other environment variables for Appium using this same method. You can also specify other options, such as custom desired capabilities that Appium should default to, and custom ways of exporting Appium logs.

For my tests, I decided to use serviceBuilder.usingAnyFreePort();, so that when my tests run, the Appium server will use any available port, rather than insisting on using port 4723. This way, if I have a forgotten Appium server running in another window, it won't interfere with my tests.

If Appium starts on a random port, our code to start a session needs to know what the port is so the client can connect to it. To solve this, I connect the client this way:

serviceBuilder.usingAnyFreePort();
server.start();
driver = new IOSDriver<MobileElement>(server.getUrl(), caps);

The AppiumDriverLocalService object has a getUrl() method which will return the URL and port of the Appium server it started.

There are more methods you can use to customize the Appium server programatically. Check out the documentation here and try experimenting: https://appium.github.io/java-client/io/appium/java_client/service/local/AppiumServiceBuilder.html

Here's the full example, putting all this together for a sample test. As usual the full example code is also available on github.

import io.appium.java_client.AppiumDriver;
import io.appium.java_client.MobileElement;
import io.appium.java_client.ios.IOSDriver;
import io.appium.java_client.service.local.AppiumDriverLocalService;
import io.appium.java_client.service.local.AppiumServiceBuilder;
import org.junit.*;
import org.openqa.selenium.remote.DesiredCapabilities;

import java.io.File;
import java.util.HashMap;

import static junit.framework.TestCase.assertTrue;

public class Edition071_AppiumServiceBuilder {
    private String APP = "https://github.com/cloudgrey-io/the-app/releases/download/v1.9.0/TheApp-v1.9.0.app.zip";

    private AppiumDriver driver;
    private static AppiumDriverLocalService server;

    @BeforeClass
    public static void startAppiumServer() {
        AppiumServiceBuilder serviceBuilder = new AppiumServiceBuilder();
        // Use any port, in case the default 4723 is already taken (maybe by another Appium server)
        serviceBuilder.usingAnyFreePort();
        // Tell serviceBuilder where node is installed. Or set this path in an environment variable named NODE_PATH
        serviceBuilder.usingDriverExecutable(new File("/Users/jonahss/.nvm/versions/node/v12.1.0/bin/node"));
        // Tell serviceBuilder where Appium is installed. Or set this path in an environment variable named APPIUM_PATH
        serviceBuilder.withAppiumJS(new File("/Users/jonahss/.nvm/versions/node/v12.1.0/bin/appium"));
        // The XCUITest driver requires that a path to the Carthage binary is in the PATH variable. I have this set for my shell, but the Java process does not see it. It can be inserted here.
        HashMap<String, String> environment = new HashMap();
        environment.put("PATH", "/usr/local/bin:" + System.getenv("PATH"));
        serviceBuilder.withEnvironment(environment);

        server = AppiumDriverLocalService.buildService(serviceBuilder);
        server.start();
    }

    @Before
    public void startSession() {
        DesiredCapabilities caps = new DesiredCapabilities();
        caps.setCapability("platformName", "iOS");
        caps.setCapability("platformVersion", "12.2");
        caps.setCapability("deviceName", "iPhone Xs");
        caps.setCapability("automationName", "XCUITest");

        caps.setCapability("app", APP);

        driver = new IOSDriver<MobileElement>(server.getUrl(), caps);
    }

    @After
    public void endSession() {
        try {
            driver.quit();
        } catch (Exception ign) {}
    }

    @AfterClass
    public static void stopAppiumServer() {
        server.stop();
    }

    @Test
    public void test() {
        // test code goes here
        assertTrue(true);
    }
}