SkarpSkarp

Chapter 8 of 11

Framework Design: Reusable, Cross-Platform Java Test Architecture

Ad-hoc scripts don’t scale. Turn your scattered tests into a maintainable Java framework that drives Android and iOS from a single, clean architecture.

15 min readen

Step 1: Why You Need a Cross-Platform Java Test Framework

From Scripts to Frameworks

Ad-hoc Appium scripts break down once you add iOS, multiple devices, and CI. A structured Java framework makes tests maintainable and scalable.

Key Benefits

A cross-platform framework gives you separation of concerns, platform abstraction, centralized configuration, and consistent reporting for Android and iOS.

Current Tooling (2026)

Most Java mobile frameworks use Appium 2.x with JUnit 5 or TestNG, integrating with CI. Espresso and XCUITest are accessed via Appium’s platform drivers.

Step 2: High-Level Architecture and Layers

Layered Architecture

Design your framework in layers: Drivers at the bottom, then Core Utilities, then Screen Objects, and Tests at the top.

Driver Layer Role

The driver layer creates and manages AppiumDriver instances, handles capabilities, and manages driver lifecycle per test or class.

Screen Objects and Tests

Screen objects wrap locators and flows into business methods. Tests call these methods and perform assertions, avoiding raw driver calls.

Step 3: Implementing the Driver Factory (Android + iOS)

A driver factory centralizes how you create drivers for Android and iOS. With Appium 2.x (current standard as of 2026), you typically connect to a running Appium server, local or remote.

Below is a simplified Java example using a factory pattern. It assumes you already have Appium 2.x installed and running, and that you know your app package/bundle and activity/launchable identifiers.

```java

// src/main/java/framework/driver/PlatformType.java

package framework.driver;

public enum PlatformType {

ANDROID,

IOS

}

```

```java

// src/main/java/framework/config/Config.java

package framework.config;

import java.util.Optional;

public class Config {

public static String get(String key, String defaultValue) {

return Optional.ofNullable(System.getProperty(key))

.orElse(Optional.ofNullable(System.getenv(key))

.orElse(defaultValue));

}

public static String getAppiumUrl() {

return get("APPIUM_URL", "http://127.0.0.1:4723");

}

public static String getPlatformName() {

return get("PLATFORM", "ANDROID");

}

}

```

```java

// src/main/java/framework/driver/DriverFactory.java

package framework.driver;

import framework.config.Config;

import io.appium.java_client.AppiumDriver;

import io.appium.java_client.android.AndroidDriver;

import io.appium.java_client.ios.IOSDriver;

import org.openqa.selenium.remote.DesiredCapabilities;

import java.net.MalformedURLException;

import java.net.URL;

public class DriverFactory {

public static AppiumDriver createDriver() {

PlatformType platform = PlatformType.valueOf(

Config.getPlatformName().toUpperCase()

);

try {

URL serverUrl = new URL(Config.getAppiumUrl());

DesiredCapabilities caps = new DesiredCapabilities();

switch (platform) {

case ANDROID:

caps.setCapability("platformName", "Android");

caps.setCapability("automationName", "UiAutomator2");

caps.setCapability("appPackage", Config.get("ANDROIDAPPPACKAGE", "com.example.app"));

caps.setCapability("appActivity", Config.get("ANDROIDAPPACTIVITY", ".MainActivity"));

caps.setCapability("deviceName", Config.get("ANDROIDDEVICENAME", "Android Emulator"));

return new AndroidDriver(serverUrl, caps);

case IOS:

caps.setCapability("platformName", "iOS");

caps.setCapability("automationName", "XCUITest");

caps.setCapability("bundleId", Config.get("IOSBUNDLEID", "com.example.app"));

caps.setCapability("deviceName", Config.get("IOSDEVICENAME", "iPhone 15"));

return new IOSDriver(serverUrl, caps);

default:

throw new IllegalArgumentException("Unsupported platform: " + platform);

}

} catch (MalformedURLException e) {

throw new RuntimeException("Invalid Appium URL", e);

}

}

}

```

Key points:

  • `Config` reads from system properties or environment variables, avoiding hard-coded values.
  • `PlatformType` is an enum that keeps platforms explicit.
  • `DriverFactory.createDriver()` hides platform-specific capability details from the rest of the framework.

Step 4: Centralized Configuration for Platforms, Environments, Devices

Why Centralize Config?

Avoid hard-coded URLs, credentials, and device names. A single config module lets you switch environments and platforms without touching tests.

Config Sources

Use a clear priority: system properties, environment variables, then defaults or properties files. Provide typed getters like getBaseUrl().

Environment-Aware URLs

Map ENV values like dev, staging, prod to base URLs in code or config files. Tests stay environment-agnostic.

Step 5: Abstracting Platform Differences with Interfaces

Hide Platform Differences

Define interfaces like LoginScreen, then create Android and iOS implementations. Tests depend on the interface, not the platform details.

Platform-Specific Implementations

Each implementation uses its own locators and flows but exposes the same methods, such as login() and isErrorVisible().

Screen Factory

A factory chooses the correct implementation at runtime based on PlatformType, keeping tests platform-agnostic.

Step 6: Structuring Tests with TestNG or JUnit 5

Base Test Class

Create a BaseTest that sets up and tears down the Appium driver using @BeforeMethod/@AfterMethod (TestNG) or @BeforeEach/@AfterEach (JUnit 5).

Tagged Tests

Use TestNG groups or JUnit 5 tags like "smoke" and "login" to organize mobile test suites for CI pipelines.

Platform-Agnostic Test Code

Tests call ScreenFactory.loginScreen(driver, platform) and work for both Android and iOS without changing assertions.

Step 7: Reporting, Logs, and Screenshots on Failure

Why Diagnostics Matter

Mobile tests fail for device- or OS-specific reasons. Centralized screenshots and logs are essential to debug flaky or rare failures.

Screenshot Utility

Implement a ScreenshotUtils class that saves PNG files with test name and timestamp into a known folder like build/screenshots.

Listeners and Extensions

Use TestNG listeners or JUnit 5 extensions to automatically take screenshots on test failure, keeping test methods clean.

Step 8: Thought Exercise – Evolving the Framework

Imagine your team has just added this Java-based cross-platform framework to your CI pipeline. Over the next six months, the following changes happen:

  1. Product managers want separate smoke, regression, and device-compatibility suites.
  2. You start using a cloud device farm (e.g., BrowserStack or Sauce Labs) in addition to local emulators/simulators.
  3. Your app adds a new biometric login flow that behaves differently on Android and iOS.

Think through and jot down answers to these questions:

  1. Suites and tagging
  • How would you use TestNG groups or JUnit 5 tags to define these three suites without duplicating tests?
  • Which tags would you introduce, and how would CI jobs select them?
  1. Device farm integration
  • Which parts of your framework should change when switching from local Appium to a cloud provider?
  • How can you keep tests and screen objects unchanged while supporting both execution modes?
  1. Biometric login differences
  • Where should Android vs iOS biometric differences live: in tests, in screen objects, or in a separate abstraction?
  • Sketch an interface and two implementations that would keep tests platform-agnostic.

Pause for a few minutes and outline your answers. Try to reference specific layers: driver, config, utilities, screen objects, tests.

Step 9: Quick Check on Architecture and Abstraction

Answer this question to check your understanding of where responsibilities should live in the framework.

You need to add support for running tests on a new iOS device type in a cloud device farm. Where is the BEST primary place to make this change so that test code and screen objects stay untouched?

  1. Inside each test class, by setting device capabilities before calling the driver
  2. In the driver and configuration layers, by adding new capability sets and config options
  3. In every screen object, by adding conditional logic for the new device type
  4. In the screenshot utility, so screenshots know which device they came from
Show Answer

Answer: B) In the driver and configuration layers, by adding new capability sets and config options

Device details and capabilities belong in the driver and configuration layers. Tests and screen objects should remain device-agnostic. You adjust configuration (e.g., new ENV or device name) and driver factory logic to create drivers for the new device type.

Step 10: Key Term Review

Flip through these flashcards to reinforce the core concepts of cross-platform Java test architecture.

Driver Layer
The lowest layer that creates and manages AppiumDriver instances, sets capabilities, and knows about platform and device details.
Core Utilities
Reusable helpers such as waits, configuration loaders, logging, and screenshot utilities that are independent of specific screens.
Screen (Page) Object
A class that encapsulates locators and user flows for a particular screen or feature, exposing business-level methods like loginAs(user).
Platform Abstraction
The practice of hiding Android vs iOS differences behind common interfaces and factories so tests stay platform-agnostic.
Centralized Configuration
A single module that manages environment, platform, and device settings, typically via system properties, environment variables, and config files.
Test Tagging (Groups)
Using TestNG groups or JUnit 5 tags to categorize tests (e.g., smoke, regression, android-only) for selective execution in CI.
Test Listener / Extension
A hook mechanism (TestNG ITestListener, JUnit 5 TestWatcher) that reacts to test events like failures to capture screenshots or logs.

Key Terms

Appium 2.x
The current major version of the Appium mobile automation framework (as of 2026), which separates drivers from the core server and emphasizes plugin-based architecture.
Device Farm
A cloud-based service providing access to many real and virtual mobile devices for automated testing (e.g., BrowserStack, Sauce Labs, LambdaTest).
JUnit 5 Tags
Annotations like @Tag("smoke") that categorize JUnit 5 tests for filtered execution.
TestNG Groups
A TestNG feature that lets you assign tests to named groups (e.g., smoke, regression) to run selected subsets.
Driver Factory
A design pattern that centralizes creation and configuration of WebDriver/AppiumDriver instances, often based on platform or environment.
Screenshot Utility
A helper component that captures and stores screenshots (usually on failure) with consistent naming and paths for reporting.
Platform Abstraction
An architectural approach where differences between platforms (Android, iOS) are hidden behind interfaces and factories, so higher layers do not depend on platform-specific details.
Centralized Configuration
A single point in the framework responsible for reading and providing configuration values (platform, environment, device, URLs) to the rest of the code.
Test Listener / Extension
Framework-specific hooks (e.g., TestNG ITestListener, JUnit 5 extensions) that run custom logic on events such as test start, success, or failure.
Screen Object (Page Object)
A class modeling a mobile screen or web page, encapsulating locators and interactions, and exposing higher-level business actions.

Finished reading?

Test your understanding with a custom practice exam on this chapter.

Test yourself