Developing a Test Automation Framework for Appium

Reading Time : 14min read
Developing a Test Automation Framework for Appium

Developing a test automation framework for Appium boils down to a simple 2-step process:

  1. Identify the Ui Element locator(address)
  2. Perform an action on it.

In the real world, Appium is used to automate an entire mobile application and the simple idea to put all element locators and interactions with locators into one file won’t help us. It’s just a bad design. When we inevitably go back to increase automation test coverage, we would likely end up with an unmaintainable project – large project files, complex code and duplicate usage of element locators will become the bane of your daily automation life.

Moreover, even a small change in the application UI would break the existing working locators, and if we use the linear structure in our test code, it will become so difficult to fix that locator because we need to replace the invalid locator from each place in the code.

For Example, most apps or websites have a ‘home’ page, such as a dashboard, containing a number of menu options. Many automation test cases might click on these menu options to perform a specific task. Now imagine that the UI is changed/revamped and menu buttons are relocated and some of them removed. This will lead to automation tests failure because scripts will not be able to find the particular element.

So in order to reduce that pain, we need to use some kind of structure which can eliminate those difficulties. And that masterpiece code structure (or framework as it’s more commonly called!) is known as Page Object Modeling.

Appium eBook

What is Appium Page Object Modeling (POM)?

Page Object Model (POM) is a widely used design pattern in Appium (and Selenium) test automation. It enhances test maintenance and reduces code duplication by keeping locators and test cases separate. Essentially, a Page Object is an object-oriented class that encapsulates all the UI element locators for a specific page (or screen) of your Application Under Test, along with the methods to interact with these elements. This structure ensures that if a UI change breaks the tests, you only need to update the corresponding Page Object class rather than every individual test script.

What does the POM automation framework look like?

The basic structure of a Page Object Model in an Appium framework typically includes:

  • WebDriver Categories: (In some cases) A separate module to handle WebDriver instantiation and configuration, which is more relevant in web automation.
  • Page Objects: Classes that encapsulate the UI element locators and the interaction methods for a particular page.
  • Test Cases: Scripts that use these Page Objects to execute test scenarios.
  • Utility Classes: Helper classes that provide common functionality across tests.
Page Objects flow chart
Figure-1: Page Object Modeling StructurePOM.

NOTE: The above structure just illustrates one possible Page Object Model structure – It may vary according to the needs of your app and test cases, and POM works best for multi page web application.

However, for simplicity—especially when starting out—we can use a “light-weight” version of POM. In this modified structure, we remove the separate WebDriver entity and include WebDriver creation logic in the BaseTestCase. This approach is particularly suitable for mobile automation (iOS and Android), where you typically deal with a more limited set of devices compared to multiple browsers in web automation.
w:

Page objects, Utility class, and test cases flow chart
Figure-2: Page Object Modeling Structure(Light-weight).
Free Appium Course

Follow the below steps to implement this (Steps shown below remain same for Intellij IDEA and Eclipse IDE):

  1. Create a new Java Project:
Create Java Project image

Figure-3: Create Java Project.

2. When you click on the Create button it will build the whole project and link the default dependencies.

3. As we are using Maven as a build tool, the first thing you need to do is to add the Maven dependencies and Import the changes.

build Maven image

Figure-4: build Maven.

4. You can observe that there is a src directory created by default project structure

project structure image


Figure 5: Project structure

We will walk through each piece of code so you understand how it all fits together.

The pom.xml

Below is a minimal pom.xml showing how to include Appium Java Client 8+ (which supports Appium 2.0), TestNG, and related dependencies. Adjust versions as needed.

<project

       xmlns="http://maven.apache.org/POM/4.0.0"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="http://maven.apache.org/POM/4.0.0

                       http://maven.apache.org/xsd/maven-4.0.0.xsd">

   <modelVersion>4.0.0</modelVersion>

   <groupId>org.kobiton</groupId>

   <artifactId>POM_Automation_Framework</artifactId>

   <version>1.0-SNAPSHOT</version>

   <properties>

       <maven.compiler.source>17</maven.compiler.source>

       <maven.compiler.target>17</maven.compiler.target>

       <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

   </properties>

   <dependencies>

       <!-- Appium Java Client 8.3.0 (brings in Selenium 4.28.x) -->

       <dependency>

           <groupId>io.appium</groupId>

           <artifactId>java-client</artifactId>

           <version>9.3.0</version>

       </dependency>

       <!-- TestNG -->

       <dependency>

           <groupId>org.testng</groupId>

           <artifactId>testng</artifactId>

           <version>7.7.1</version>

           <scope>test</scope>

       </dependency>

       <dependency>

           <groupId>org.seleniumhq.selenium</groupId>

           <artifactId>selenium-remote-driver</artifactId>

           <version>4.28.1</version>

       </dependency>

       <!-- (Optional) SLF4J binding to remove the "NOP" logger warning -->

       <dependency>

           <groupId>org.slf4j</groupId>

           <artifactId>slf4j-simple</artifactId>

           <version>2.0.7</version>

           <scope>test</scope>

       </dependency>

   </dependencies>

   <build>

       <plugins>

           <plugin>

               <groupId>org.apache.maven.plugins</groupId>

               <artifactId>maven-surefire-plugin</artifactId>

               <version>3.0.0-M9</version>

           </plugin>

       </plugins>

   </build>

</project>

1. After adding the Appium and TestNG dependencies, we will create the Page Object package inside src > test > java

Adding new package inside src_test_java image

Figure-10: Adding new package inside src_test_java.

2. Using the same approach, add a testcases and utils package.

utils image

    Figure-12: Package Name utils.png.

    testcases image

    Figure-13: Package Name test cases.

    Adding configuration.properties

    Our first step is to create a configuration.properties file under the src/test/resources/ directory. In Java-based automation frameworks, .properties files are commonly used to centralize configuration values (like device names, app package names, or URLs) in a single location.

    This follows the Page Object Model principle of separating configuration details from the test logic. If you need to change the connected device name, for instance, you simply edit the value of the android.device.name key—no need to modify multiple test classes.

    Here’s a sample of what the configuration.properties file might contain:

    android.device.name=emulator-5554
    
    android.platform.version=11
    
    app.path=/path/to/your/app.apk

    Note: You can load these values in your tests using Java’s Properties class to retrieve each key-value pair as needed.

    image of configured properties

    Figure-14: configuration properties.

    PropertyUtils

    Goal: Provide a centralized way to read key-value pairs from configuration.properties (or other .properties files).

    Why It’s Needed: Keeps your test code DRY (Don’t Repeat Yourself). Instead of re-writing file reading logic in multiple places, you only implement it once. Makes it easy to update or extend configuration access in the future (e.g., adding environment-specific properties).

    WaitUtils

    Goal: Handle different types of waits (implicit, explicit, fluent) to synchronize your test scripts with the UI loading times.

    Why It’s Needed: Avoids flaky or brittle tests by introducing proper wait conditions (e.g., wait until a new element appears on the screen). Encapsulates repeated wait logic (e.g., for specific locators) into reusable methods.

    utils PropertyUtils image

    Figure-15: utils PropertyUtils.

    utils WaitUtils image

    Figure-16: utils WaitUtils.

    Why a BaseTestCase Class?

    In a well-structured test automation framework (especially one following the Page Object Model), a BaseTestCase class serves several important purposes:

    1. Central Driver Management:
      It creates and manages the Appium WebDriver (or AppiumDriver) instance. This ensures all tests have a consistent way of initializing, using, and cleaning up the driver.
    2. Common Setup/Teardown:
      It defines methods to perform common setup steps (e.g., loading configuration properties, preparing test data) and teardown actions (e.g., quitting the driver) for every test or test class.
    3. Reusable Test Hooks:
      By utilizing TestNG annotations like @BeforeSuite, @BeforeClass, @BeforeMethod, @AfterMethod, etc., you can handle different stages in the test lifecycle. This keeps your test classes clean and focused on the actual test logic.

    Understanding the TestNG Annotations

    Here’s a quick overview of some commonly used TestNG annotations:

    • @BeforeSuite
      Runs once before all tests in the suite (i.e., before all <test> tags in your testng.xml are executed).
    • @BeforeTest
      Runs once before each <test> tag defined in testng.xml.
    • @BeforeClass
      Runs once before the first test method in the current class.
    • @BeforeMethod
      Runs before each test method. This is a good place to do method-level setup, like resetting your app state or navigating to a specific screen.
    • @AfterMethod
      Runs after each test method. Typically used to clean up or log the test result.
    • @AfterClass
      Runs once after all test methods in the current class have been executed.
    • @AfterTest
      Runs once after each <test> tag in testng.xml.
    • @AfterSuite
      Runs once after all tests in the suite have completed.
    import org.testng.annotations.AfterMethod;
    
    import org.testng.annotations.BeforeMethod;
    
    import org.testng.annotations.Test;
    
    public class TestCases {
    
        @BeforeMethod
    
        public void setUp() {
    
            System.out.println("Before Method executed..!");
    
        }
    
        @Test
    
        public void test() {
    
            System.out.println("Test");
    
        }
    
        @AfterMethod
    
        public void tearDown() {
    
            System.out.println("After Method executed..!");
    
        }
    
    }

    Have a look at the below screenshot to get the idea how to execute the test case and refer to the second screenshot for output of the above program.

    Execute the TestNG test image

    Figure-17: Execute the TestNG test.

    You can learn more about TestNG Annotations here: https://www.tutorialspoint.com/testng/testng_basic_annotations.htm

    The idea is to put the code for creation of the WebDriver object inside @BeforeMethod, because we want the webdriver object in place before starting the test case(method). Please refer below code:

    @BeforeMethod
    
    public void setUpAppium() throws MalformedURLException {
    
        DesiredCapabilities capabilities = new DesiredCapabilities();
    
        setDesiredCapabilitiesForAndroid(capabilities);
    
        driver = new AppiumDriver(new URL(APPIUM_SERVER_URL), capabilities);
    
    }
    
    /**
    
     * It will set the DesiredCapabilities for the local execution
    
     *
    
     * @param desiredCapabilities
    
     */
    
    private void setDesiredCapabilitiesForAndroid(DesiredCapabilities desiredCapabilities) {
    
    String PLATFORM_NAME = PropertyUtils.getProperty("android.platform");
    
    String PLATFORM_VERSION = PropertyUtils.getProperty("android.platform.version");
    
    String APP_NAME = PropertyUtils.getProperty("android.app.name");
    
    String APP_RELATIVE_PATH = PropertyUtils.getProperty("android.app.location") + APP_NAME;
    
    String APP_PATH = getAbsolutePath(APP_RELATIVE_PATH);
    
    String DEVICE_NAME = PropertyUtils.getProperty("android.device.name");
    
    String APP_PACKAGE_NAME = PropertyUtils.getProperty("android.app.packageName");
    
    String APP_ACTIVITY_NAME = PropertyUtils.getProperty("android.app.activityName");
    
    String APP_FULL_RESET = PropertyUtils.getProperty("android.app.full.reset");
    
    String APP_NO_RESET = PropertyUtils.getProperty("android.app.no.reset");
    
    desiredCapabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, "uiautomator2");
    
    desiredCapabilities.setCapability(MobileCapabilityType.DEVICE_NAME, DEVICE_NAME);
    
    desiredCapabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, PLATFORM_NAME);
    
    desiredCapabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, PLATFORM_VERSION);
    
    desiredCapabilities.setCapability(MobileCapabilityType.APP, APP_PATH);
    
    desiredCapabilities.setCapability(AndroidMobileCapabilityType.APP_PACKAGE, APP_PACKAGE_NAME);
    
    desiredCapabilities.setCapability(AndroidMobileCapabilityType.APP_ACTIVITY, APP_ACTIVITY_NAME);
    
    desiredCapabilities.setCapability(MobileCapabilityType.FULL_RESET, APP_FULL_RESET);
    
    desiredCapabilities.setCapability(MobileCapabilityType.NO_RESET, APP_NO_RESET);
    
            desiredCapabilities.setCapability(AndroidMobileCapabilityType.AUTO_GRANT_PERMISSIONS, true);
    
    }
    
    // To get Absolute Path from Relative Path
    
    private static String getAbsolutePath(String appRelativePath){
    
        File file = new File(appRelativePath);
    
        return file.getAbsolutePath();
    
    }
    
    /**
    
    * This will quite the android driver instance
    
    */
    
    private void quitDriver() {
    
        try {
    
     	this.driver.quit();
    
    } catch (Exception e) {
    
     e.printStackTrace();
    
    }
    
    }

    This code illustrates how you can leverage the @BeforeMethod and @AfterMethodto leverage WebDriver object creation and deletion.

    Every Test Case class file extends the Above BaseTest Class file, so all the methods in the BaseTest class file are visible and applicable to extending classes(TestCases) and eventually you don’t need to provide any extra code at your TestCase level to manage the WebDriver creation and deletion. You just have to focus on Test methods creation on the TestCases.java file.

    4. Add a BasePO Class under the pageobject package.

    • BasePO is the class containing the PageFactory method.

    5. Below code is responsible to initialize the web elements:

    private void initElements() {
    
       PageFactory.initElements(new AppiumFieldDecorator(driver, Duration.ofSeconds(IMPLICIT_WAIT)), this);
    
    }

    Now every other Page Object class like LoginPO will extend the BasePO class, so the constructor of BasePO is always called first and initElements method will be called on BasePO constructor. In simple language, initElements will always called first whenever any Page Object class gets called.

    BasePO Image

    Figure-19: BasePO.

    Here you can also see some objects and variables defined. IMPLICIT_WAIT is getting the values defined in the properties file of java which will be stored under resources/ dir.

    In the Page Object class, you need to define the element’s locators using the below approach:

    For iOS:

    @iOSFindBy(xpath =//XCUIElementTypeTextField”)
    
    IOSElement emailTextField;

    For Android:

    @AndroidFindBy(xpath = “//android.widget.TextView[@text=’Login Screen’]”)

    AndroidElement loginScreenTextView;

    In this example, we will work with the sample Android application and we will use the UiAutomatorViewer inspection tool as it is a speedy way to get the element’s locator.

    In the below screenshot we get the locator for Login Screen Textview.

    image of  UiAutomatorViewer for Android Application

    Figure-20: UiAutomatorViewer for Android Application.

    And you can create methods in the Page Object class (like in LoginPO) for the element locators, for example, if you want to Tap on Login TextView, then you can create a method such as tapOnLoginScreenTextView().

    HomeScreenPO.java

    package pageobject;
    
    import io.appium.java_client.AppiumDriver;
    
    import io.appium.java_client.android.AndroidElement;
    
    import io.appium.java_client.pagefactory.AndroidFindBy;
    
    public class HomeScreenPO extends BasePO {
    
        public HomeScreenPO(AppiumDriver driver) {
    
            super(driver);
    
        }
    
        @AndroidFindBy(xpath = "//android.widget.TextView[@text='Login Screen']")
    
        AndroidElement loginScreenTextView;
    
        /**
    
         * This method will click on Login Screen textview.
    
         */
    
        public void tapOnLoginScreenTextView(){
    
            loginScreenTextView.click();
    
        }
    
    }
    image of  HomeScreen page object

    Figure-21: HomeScreenPO.png

    6. Now that we have created our first Page Object class and added our first locator into it, we are ready to create a very basic simple test on TestCase

    7. Before creating the test case you need to provide the correct path to the application. You can either:

    1. Provide the application locally in the code.
      desiredCapabilities.setCapability(MobileCapabilityType.APP, “path/to/.apk or .ipa(or .app) file”);
    2. Provide the URL of the application.
      desiredCapabilities.setCapability(MobileCapabilityType.APP, “https://github.com/cloudgrey-io/the-app/releases/download/v1.7.0/TheApp-v1.7.0.apk”);
    image of coping local apk file
    Figure-22: Local apk file.

    In our example we will use the locally resided .apk file.

    NOTE: Before executing the Test Case make sure that the Appium mobile automation test server is running on https://127.0.0.1:4723/wd/hub

    TestCases.java
    
    package testcases;
    import org.testng.annotations.AfterMethod;
    import org.testng.annotations.BeforeMethod;
    import org.testng.annotations.BeforeTest;
    import org.testng.annotations.Test;
    import pageobject.HomeScreenPO;
    
    /**
     * Year: 2018-19
     *
     * @author Prat3ik on 23/11/18
     * @project POM_Automation_Framework
     */
    public class TestCases extends BaseTest{
        @Test
        public void test() {
            HomeScreenPO homeScreenPO = new HomeScreenPO(driver);
            homeScreenPO.tapOnLoginScreenTextView();
        }
        @BeforeTest
        @Override
        public void setUpPage() {
        }
    }

    Execute the above test by selecting the test method > Right Click > Run ‘test()’

    image of Test Case successful execution
    Figure-23: Test Case successful execution.

    If you can execute the test case successfully, then you will get the above screenshot.

    Executing the Test Case

    Once you’ve completed all the setup and run your test, you should see a success message or screenshot similar to the one shown above.

    Code Repository

    You can find the complete code for this framework here: (GitHub link) 

    Why So Much Work for a Simple Test?

    It might seem like a lot of setup for a single test, but remember that this groundwork enables quick scaling. Adding new test cases now simply means creating a new Test class and leveraging your existing Page Objects and utilities.

    The Real Benefit—Maintenance

    Imagine your next release changes the UI and breaks a locator. With this framework, you only need to update that one locator in the relevant Page Object (e.g., HomeScreenPO.java if the login button has changed). There’s no need to hunt through every test file, which saves you significant time and frustration.

    How to Update a Locator

    1. Open the Affected Page Object
      • In this example, if the Login button’s locator changes, open the HomeScreenPO.java file.
    2. Find the New Locator
      • Use Appium Inspector (Android) or Accessibility Inspector (iOS) to identify the updated locator.
    3. Replace the Old Locator
      • Update the @FindBy or By declaration in HomeScreenPO.java. That’s it—your test should now work with the new UI element.

    This maintainability is one of the greatest strengths of the Page Object Model: you spend time upfront, but you save exponentially more time when your app evolves (which it always does!).

    image of android application login screen locator change
    Figure-24: Android Application Locator Change.

    Change the Locator: After getting the right locator you just need to change it with an old and incorrect locator, so In HomeScreenPO.java file you just need to replace the old locator with the new locator.

    @AndroidFindBy(xpath = "//android.widget.Button[@text='Login Screen']")
    AndroidElement loginScreenTextView;

    That’s it! As you can see, we just need to change the locator in the Page Object class and everything works normally again – that is the beauty of using this Appium framework! If you are not using the Appium framework, then you might need to change the locator from every affected place in code, which is not advisable and can also break something else.

    Structuring your test cases like this will make for a far more maintainable test automation suite. Learning the rigors to do it following this design pattern will benefit you and your organization for years to come!

    Get a free trial from Kobiton