Starting Appium Programmatically with AppiumServiceBuilder

6 min read

Running Appium as a separate process that you start manually before tests is fine for local development. For CI and for clean test isolation, starting the Appium server programmatically inside the test suite is better — no pre-running process to forget, no port conflicts, automatic cleanup on suite exit.

AppiumServiceBuilder basics

AppiumServiceBuilder is part of io.appium.java_client.service.local. It constructs an AppiumDriverLocalService that wraps the Appium Node process.

import io.appium.java_client.service.local.AppiumDriverLocalService;
import io.appium.java_client.service.local.AppiumServiceBuilder;
import io.appium.java_client.service.local.flags.GeneralServerFlag;
 
import java.io.File;
 
public class AppiumServer {
 
    private static AppiumDriverLocalService service;
 
    public static void start() {
        service = new AppiumServiceBuilder()
            .withIPAddress("127.0.0.1")
            .usingPort(4723)
            .withArgument(GeneralServerFlag.BASEPATH, "/")
            .withLogFile(new File("target/appium.log"))
            .build();
 
        service.start();
    }
 
    public static void stop() {
        if (service != null && service.isRunning()) {
            service.stop();
        }
    }
 
    public static String getServiceUrl() {
        return service.getUrl().toString();
    }
}

The withLogFile call redirects Appium's verbose output to target/appium.log — grep this file first when a session fails to start.

Hooking into TestNG lifecycle

Start the server in @BeforeSuite and stop it in @AfterSuite. These run once per suite, not once per test:

package com.example.base;
 
import org.testng.annotations.AfterSuite;
import org.testng.annotations.BeforeSuite;
 
public abstract class BaseTest {
 
    @BeforeSuite(alwaysRun = true)
    public void startServer() {
        AppiumServer.start();
    }
 
    @AfterSuite(alwaysRun = true)
    public void stopServer() {
        AppiumServer.stop();
    }
 
    @BeforeTest
    @Parameters("platform")
    public void setUp(String platform) {
        DriverManager.initDriver(platform, AppiumServer.getServiceUrl());
    }
 
    @AfterTest
    public void tearDown() {
        DriverManager.quitDriver();
    }
}

alwaysRun = true ensures server cleanup runs even when tests fail.

Pointing DriverManager at the service URL

Pass the service URL into DriverManager.initDriver so it uses the programmatic server rather than a hardcoded localhost URL:

public static void initDriver(String platform, String serverUrl) {
    try {
        URL url = new URL(serverUrl);
        if ("Android".equalsIgnoreCase(platform)) {
            UiAutomator2Options options = buildAndroidOptions();
            driverThreadLocal.set(new AndroidDriver(url, options));
        } else {
            XCUITestOptions options = buildIosOptions();
            driverThreadLocal.set(new IOSDriver(url, options));
        }
    } catch (MalformedURLException e) {
        throw new RuntimeException("Bad server URL: " + serverUrl, e);
    }
}

Handling port conflicts

If port 4723 is already in use (another Appium process, another test run), service.start() throws. Two approaches:

Use a random free port:

service = new AppiumServiceBuilder()
    .usingAnyFreePort()
    .build();
service.start();
String url = service.getUrl().toString(); // e.g., http://127.0.0.1:49215

usingAnyFreePort() asks the OS for an available port. This is the right default for CI where multiple builds may run in parallel.

Explicitly check before starting:

public static void start() {
    if (service != null && service.isRunning()) {
        return; // already up
    }
    // ...build and start
}

Environment requirements

AppiumServiceBuilder needs Node.js and the Appium binary on the system PATH. In CI, install them in the setup step:

- run: npm install -g appium
- run: appium driver install uiautomator2
- run: appium driver install xcuitest

On macOS, verify with which appium and appium --version. If AppiumServiceBuilder can't find Appium, it throws AppiumServerHasNotBeenStartedLocallyException — the message includes the resolved path it tried, which tells you exactly what's missing from PATH.

Reading the Appium log

target/appium.log is your first debug resource. Look for:

  • Appium REST http interface listener started — server is up
  • Could not find a connected Android device — capability or ADB issue
  • WebDriverAgent installation failed — iOS provisioning issue
  • Session ID appearing then disappearing — driver quit before test finished

// tip to track lessons you complete and pick up where you left off across devices.