RizTech Academy logo
RizTech Academy
Mobile Automation with AppiumLesson 2 of 535 min

Your first mobile test

With the architecture clear, let's write a mobile test. The shape will feel familiar — find an element, act on it, assert — because Appium uses the same WebDriver ideas as web automation. What differs is the setup (capabilities, a session) and the way you locate elements on a native screen. This lesson walks through a first Appium test end to end, using WebdriverIO as the client and the AgentPay agent app as the example.

The setup: a session from capabilities

A web test just needs a page; a mobile test must first start a session — connect to the Appium server with your capabilities, which launches the app on the device. With WebdriverIO this is configured in wdio.conf.js, but shown inline the idea is:

import { remote } from 'webdriverio';

const driver = await remote({
  hostname: 'localhost',
  port: 4723,                       // the Appium server
  capabilities: {
    platformName: 'Android',
    'appium:automationName': 'UiAutomator2',
    'appium:deviceName': 'Pixel_7_API_34',
    'appium:app': '/path/to/agentpay-agent.apk',
  },
});

driver is your handle to the running app — the mobile equivalent of Playwright's page. At the end you call await driver.deleteSession() to close it. (In practice WebdriverIO's test runner manages this for you via config and hooks — the fixtures idea, for mobile.)

Your first test

Here is a login test for the AgentPay agent app — the same flow you automated on the web, now native:

import { expect } from '@wdio/globals';

describe('AgentPay agent login', () => {
  it('signs in and shows the dashboard', async () => {
    // find by accessibility id — the mobile equivalent of a user-facing locator
    const email = await driver.$('~email-field');
    const password = await driver.$('~password-field');
    const signIn = await driver.$('~sign-in-button');

    await email.setValue('asha@agentpay.test');
    await password.setValue('operator123');
    await signIn.click();

    const heading = await driver.$('~dashboard-heading');
    await expect(heading).toBeDisplayed();   // web-first-style assertion, waits for it
  });
});

Point by point:

  • driver.$('~name') finds one element; the ~ prefix means accessibility id (below) — the robust, user-facing way to locate on mobile.
  • setValue types into a field; click taps. These are the mobile actions, and like Appium generally they wait for the element within the configured timeout.
  • expect(el).toBeDisplayed() asserts the element is on screen, retrying — the mobile counterpart of Playwright's web-first assertions.

The structure — find, act, assert — is exactly what you already know. Only the locator syntax and the setup differ.

Waiting on mobile

Mobile apps are asynchronous too (a screen loads, a spinner shows), and the same rule from the web module applies: wait for the condition, never sleep. Appium/WebdriverIO give you waits:

const heading = await driver.$('~dashboard-heading');
await heading.waitForDisplayed({ timeout: 5000 });   // wait until it appears

Use waitForDisplayed, waitForEnabled, waitForExist to wait for the state you need, rather than a fixed driver.pause(2000). Mobile is, if anything, more prone to timing flakiness than web (devices are slower and more variable), so disciplined waiting matters even more here. pause exists; it is for debugging only.

Running it

With the Appium server running (appium) and an emulator booted, you run the test through WebdriverIO:

npx wdio run wdio.conf.js

The first run is where mobile bites: a wrong capability, an unbooted emulator, a missing driver, or a bad app path will stop it before a single assertion. Read the error to find which hop failed (the architecture lesson): "could not start session" is usually capabilities or device; "element not found" is a locator or a screen that had not loaded. Getting the first test to connect and pass is genuinely the hardest step — after that, writing more tests is much like the web.

Check your work

Session first. A mobile test starts a session from capabilities (remote({ capabilities })), which launches the app; driver is your handle (the mobile page), closed with deleteSession (usually managed by the runner via config/hooks).

First test: find (driver.$('~accessibility-id')), act (setValue to type, click to tap), assert (expect(el).toBeDisplayed(), retrying). Same find-act-assert as web; only locator syntax and setup differ.

Wait for conditions, never sleep — waitForDisplayed/waitForEnabled/waitForExist, not pause(2000). Mobile is more timing-flaky than web (slower, variable devices), so disciplined waiting matters more. pause is debug-only.

Running: Appium server up + emulator booted, then npx wdio run. The first connection is the hard part — read the error for which hop failed (session start = capabilities/device; element not found = locator/screen not loaded).

Practice

  1. Write the capabilities and a session-start snippet for the AgentPay agent app on an Android emulator.
  2. Write a first login test using accessibility-id locators, setValue, click, and a toBeDisplayed assertion.
  3. Replace a driver.pause(2000) with waitForDisplayed and explain why it is both safer and faster.
  4. Given "could not create session", list three capability/device causes and how you would check each.
  5. Given "element not found: ~dashboard-heading", say what two things could be wrong and how to diagnose.
  6. Explain why the first passing mobile test is the hardest, and what gets easier afterward.

Official documentation

Next: mobile locators and gestures.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship