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.setValuetypes into a field;clicktaps. 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
- Write the capabilities and a session-start snippet for the AgentPay agent app on an Android emulator.
- Write a first login test using accessibility-id locators,
setValue,click, and atoBeDisplayedassertion. - Replace a
driver.pause(2000)withwaitForDisplayedand explain why it is both safer and faster. - Given "could not create session", list three capability/device causes and how you would check each.
- Given "element not found: ~dashboard-heading", say what two things could be wrong and how to diagnose.
- Explain why the first passing mobile test is the hardest, and what gets easier afterward.
Official documentation
- WebdriverIO — Get started — Setting up the client and running tests.
- Appium — Write a test (WebdriverIO) — A first Appium test, step by step.
- WebdriverIO — waitForDisplayed — Waiting for elements instead of pausing.
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