Back to Blog
Technology
June 21, 2025
3 min read
557 words

Appium Page Object Model: Best Practices for Scalable Tests

Learn how to implement a robust Page Object Model pattern in your Appium test automation framework for maintainable and scalable mobile tests.

Appium Page Object Model: Best Practices for Scalable Tests

Why Page Object Model Matters for Appium

After maintaining test automation frameworks for years, I have learned one fundamental truth: without proper architecture, your test suite becomes unmaintainable. The Page Object Model (POM) is not just a design pattern—it is the foundation of professional mobile test automation.

Core Principles of POM

1. Separation of Concerns

  • Page classes: Encapsulate element locators and page operations
  • Test classes: Focus purely on test logic and assertions
  • Utilities: Handle common operations like waits and gestures

2. Single Responsibility

Each page object should represent one logical screen or component:

  • LoginPage handles login functionality
  • HomePage handles navigation and main features
  • SearchPage handles search operations

Base Page Class Implementation

// base.page.js
class BasePage {
  constructor(driver) {
    this.driver = driver;
    this.timeout = 30000;
  }

  async waitForElement(locator) {
    const element = await this.driver.$(locator);
    await element.waitForDisplayed({ timeout: this.timeout });
    return element;
  }

  async click(locator) {
    const element = await this.waitForElement(locator);
    await element.click();
  }

  async type(locator, text) {
    const element = await this.waitForElement(locator);
    await element.setValue(text);
  }

  async getText(locator) {
    const element = await this.waitForElement(locator);
    return await element.getText();
  }

  async isDisplayed(locator) {
    try {
      const element = await this.driver.$(locator);
      return await element.isDisplayed();
    } catch (error) {
      return false;
    }
  }

  async swipeUp() {
    const { width, height } = await this.driver.getWindowSize();
    await this.driver.execute('mobile: swipeGesture', {
      left: width / 2,
      top: height * 0.8,
      width: 0,
      height: height * 0.4,
      direction: 'up',
      percent: 0.75
    });
  }
}

Screen-Specific Page Object

// login.page.js
import BasePage from './base.page';

class LoginPage extends BasePage {
  // Locators
  get usernameField() { return '~username_input'; }
  get passwordField() { return '~password_input'; }
  get loginButton() { return '~login_button'; }
  get errorMessage() { return '~error_message'; }
  get forgotPasswordLink() { return '~forgot_password'; }

  // Actions
  async enterUsername(username) {
    await this.type(this.usernameField, username);
  }

  async enterPassword(password) {
    await this.type(this.passwordField, password);
  }

  async tapLogin() {
    await this.click(this.loginButton);
  }

  async login(username, password) {
    await this.enterUsername(username);
    await this.enterPassword(password);
    await this.tapLogin();
  }

  // Verifications
  async getErrorText() {
    return await this.getText(this.errorMessage);
  }

  async isErrorDisplayed() {
    return await this.isDisplayed(this.errorMessage);
  }
}

export default new LoginPage();

Test Class Using Page Objects

// login.test.js
import { expect } from 'chai';
import LoginPage from '../pages/login.page';
import HomePage from '../pages/home.page';

describe('Login Functionality', () => {
  beforeEach(async () => {
    await driver.launchApp();
  });

  it('should login with valid credentials', async () => {
    await LoginPage.login('testuser', 'validpassword');
    
    expect(await HomePage.isWelcomeDisplayed()).to.be.true;
  });

  it('should show error for invalid credentials', async () => {
    await LoginPage.login('invalid', 'wrong');
    
    expect(await LoginPage.isErrorDisplayed()).to.be.true;
    expect(await LoginPage.getErrorText()).to.include('Invalid');
  });

  it('should navigate to forgot password', async () => {
    await LoginPage.click(LoginPage.forgotPasswordLink);
    
    expect(await ForgotPasswordPage.isPageDisplayed()).to.be.true;
  });
});

Component-Based Page Objects

// components/navigation-bar.component.js
class NavigationBar {
  constructor(driver) {
    this.driver = driver;
  }

  get homeTab() { return '~tab_home'; }
  get searchTab() { return '~tab_search'; }
  get profileTab() { return '~tab_profile'; }

  async navigateToHome() {
    const element = await this.driver.$(this.homeTab);
    await element.click();
  }

  async navigateToSearch() {
    const element = await this.driver.$(this.searchTab);
    await element.click();
  }

  async navigateToProfile() {
    const element = await this.driver.$(this.profileTab);
    await element.click();
  }
}

// home.page.js
import BasePage from './base.page';
import NavigationBar from './components/navigation-bar.component';

class HomePage extends BasePage {
  constructor(driver) {
    super(driver);
    this.navBar = new NavigationBar(driver);
  }
}

Best Practices Summary

  • Use descriptive locators with accessibility IDs when possible
  • Keep page objects focused on single screens or components
  • Create reusable components for shared UI elements
  • Return page objects from navigation methods for fluent APIs
  • Use getter functions for lazy loading of elements
  • Implement proper wait strategies in base class
Tags:TechnologyTutorialGuide
X

Written by XQA Team

Our team of experts delivers insights on technology, business, and design. We are dedicated to helping you build better products and scale your business.