Automate a React Native App with Appium
Automate a React Native app on Android with Appium and a mock API server.
Introduction
30 Second Summary
Testing a mobile app by hand means tapping through the same screens over and over. Some scenarios are nearly impossible to set up manually, like verifying what happens when a premium subscription expired exactly 90 days ago.
In this project, you will build a complete mobile test automation setup that controls a React Native app on your physical Android phone through code. You will also create a mock API server that lets you simulate any backend state instantly.
What You'll Build
A mobile test automation environment using Appium, WebdriverIO, and json-server that automates UI interactions on a React Native "user dashboard" app running on your physical Android device.
By the end of this project, you'll have:
- A React Native app (APK) installed on your phone with testID-enabled elements for reliable automation.
- An Appium + WebdriverIO test project that controls your phone over USB, tapping buttons, filling inputs, and verifying screens.
- A json-server mock API with switchable data scenarios that simulate states like expired subscriptions, admin permissions, and holiday promos.
- Secret Mission: Automate a multi-step navigation flow with scroll gestures and cross-screen assertions.
Are there any prerequisites?
This project assumes you already have Node.js, Java, and VS Code installed on your Windows machine. You also need a physical Android 10+ phone with a USB cable.
Set Up Android SDK and Connect Your Phone
To automate a phone's UI through code, your computer needs two things. First, a way to talk to the phone over USB. Second, an automation server that translates your test scripts into real taps and swipes on the screen.
In this step, you'll install the Android SDK Platform-Tools for USB communication, configure your phone to accept automation commands, and install Appium as your automation server.
In this step, get ready to:
- Install Android SDK Platform-Tools and verify USB communication with adb.
- Enable Developer Options and USB Debugging on your Android phone.
- Install Appium and the UiAutomator2 driver globally.
Install Android SDK Platform-Tools on Windows
Android SDK Platform-Tools is a small package from Google that includes adb (Android Debug Bridge). This is the command-line tool your computer uses to communicate with your Android phone over USB.
- Open your browser and navigate to the Platform-Tools download page:
- https://developer.android.com/tools/releases/platform-tools
- Scroll down to the download section.
- Check the box to accept the terms.
- Click Download SDK Platform-Tools for Windows.
- Open File Explorer and create a new folder at C:\Android.
- Open the downloaded ZIP file and extract its contents into C:\Android\ so you end up with C:\Android\platform-tools\ containing files like adb.exe.
Why not install Android Studio?
Android Studio is a large download that includes far more than you need. The standalone Platform-Tools package is only about 15 MB and gives you adb, which is all that's required for real-device automation.
Now you need to add the Platform-Tools folder to your Windows PATH so you can run adb from any terminal window.
- Press the Windows key to open your search bar.
- Type Environment Variables and select Edit the system environment variables.
- In the System Properties window, click Environment Variables at the bottom.
- Under System variables, find and select Path.
- Click Edit.
- Click New and type C:\Android\platform-tools.
- Click OK to close the Edit window.
While you're here, create the ANDROID_HOME environment variable that Appium needs later.
- Back in the Environment Variables window, click New under System variables.
- Set Variable name to ANDROID_HOME.
- Set Variable value to C:\Android.
- Click OK on all remaining dialogs to save everything.
What is ANDROID_HOME?
ANDROID_HOME tells tools like Appium where your Android SDK lives. Appium's UiAutomator2 driver looks for this variable to locate adb automatically.
- Open a new terminal in VS Code by pressing Ctrl+` (backtick key). It must be a new terminal opened after saving the PATH changes.
- Verify the installation by running:
adb --version
✔️ I see a version number
You should see output like Android Debug Bridge version 37.0.0 or similar. Your Platform-Tools installation is working.
ⓧ 'adb' is not recognized
- Make sure you opened a NEW terminal after saving your PATH changes. Existing terminals won't pick up the update.
- Double-check that C:\Android\platform-tools is listed in your System PATH. Re-open the Environment Variables dialog and verify the entry.
- Confirm adb.exe exists inside C:\Android\platform-tools\. If the folder is empty, re-extract the ZIP.
Enable Developer Options and connect your phone
Your phone's USB Debugging mode lets your computer send commands to the phone over a USB cable. This is what Appium uses to control the device.
- On your Android phone, open Settings.
- Scroll down and tap About phone.
- Find Build Number and tap it 7 times. You'll see a toast message saying you are now a developer.
- Go back to Settings.
- Tap Developer Options (now visible near the bottom of the list).
- Toggle on USB Debugging.
- Connect your phone to your PC with the USB cable.
- Your phone may show a prompt asking "Allow USB debugging?" Tap Allow.
What is USB Debugging?
USB Debugging opens a communication channel between your phone and computer using the ADB protocol. It allows tools on your PC to install apps, read logs, and send touch commands to the phone.
- In your VS Code terminal, verify the connection by running:
adb devices
You should see output listing your device with a serial number followed by device (for example, ABC123XY device).
✔️ I see my device serial number
Your phone is connected and ready to receive automation commands. You're all set for the next part.
ⓧ I see 'unauthorized' next to my device
- Check your phone screen for the "Allow USB debugging" prompt and tap Allow.
- If you don't see the prompt, unplug and re-plug the USB cable, then run adb devices again.
ⓧ I see 'List of devices attached' with nothing below it
- Make sure USB Debugging is turned on in Developer Options on your phone.
- Try a different USB cable. Some cables are charge-only and don't support data transfer.
- When you connect the phone, a notification may appear asking you to select a USB mode. Choose File Transfer or MTP mode.
Install Appium and the UiAutomator2 driver
Appium is an open-source automation server that uses the WebDriver protocol to send commands to your phone. The UiAutomator2 driver is the plugin Appium uses specifically for Android devices.
- Install Appium globally by running:
npm install -g appium
What does this command do?
npm install -g appium installs the Appium server as a global command on your system. The -g flag makes it available from any terminal window, not just one project folder.
- Once Appium finishes installing, install the UiAutomator2 driver by running:
appium driver install uiautomator2
What is UiAutomator2?
UiAutomator2 is Google's UI testing framework for Android. When Appium receives a command like "tap this button," it passes it to UiAutomator2, which executes the action on the actual device.
It requires Android API 26 (Android 8.0) or higher, so your Android 10 phone is fully supported.
- Verify Appium installed correctly by running:
appium --version
You should see a version number like 3.5.2 or similar.
- Verify the UiAutomator2 driver is installed by running:
appium driver list
You should see uiautomator2 listed with an [installed] marker next to it.
Appium install failed or command not found?
Appium 3 requires Node.js ^20.19.0 || ^22.12.0 || >=24.0.0. Check your version by running node --version in your terminal. If your version is too old, update Node.js from nodejs.org before retrying the install.
If you see a permissions error on Windows, try closing VS Code and reopening it with Run as administrator. Then retry the npm install -g appium command.
Help me troubleshoot my Appium installation.
Your automation environment is ready. You have adb talking to your phone and Appium ready to send automation commands. Next up, you'll build and install the React Native app that your tests will automate.
Build and Install the React Native Test App
Your automation environment is ready. You have Appium installed, your phone connected, and adb verified.
Now you need an app to automate. In this step, you will build a simple React Native "user dashboard" app with Expo, add automation-friendly testID attributes to every element, and install the APK on your phone.
In this step, get ready to:
- Create an Expo account and scaffold a new React Native project.
- Replace the app code with a testID-enabled user dashboard.
- Build an APK with EAS Build and install it on your phone.
Create your Expo account and project
Expo is a React Native framework that simplifies building mobile apps. Its cloud build service (EAS Build) compiles your code into a standalone APK you can install on your phone without needing Android Studio.
- Open your browser and navigate to expo.dev.
- Click Sign Up and create a free account.
- Confirm your email address if prompted.
Why Expo and EAS Build?
Normally, building a React Native APK requires Android Studio and a full SDK installation. Your machine does not have a working GPU for the emulator, so EAS Build compiles the APK in the cloud for free.
The free plan gives you 30 builds per month, which is more than enough for this project.
- Install the EAS CLI globally by running this command in your terminal:
npm install -g eas-cli
- Log in to your Expo account by running:
eas login
What happens next?
EAS CLI will prompt you for the email and password you just used to create your Expo account. Enter them when asked.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create a new React Native project by running:
npx create-expo-app@latest user-dashboard
What will this command ask?
The CLI may prompt you to select an Expo SDK version. Select SDK 54 if prompted. This produces a stable project compatible with EAS Build for standalone APKs.
- Navigate into your new project folder by running:
cd user-dashboard
- Confirm the project was created by running:
ls
You should see files like App.js, package.json, and a node_modules folder in the output.
Command not found or download failed?
Make sure Node.js is installed and working by running node --version. Appium 3 requires Node.js ^20.19.0 || ^22.12.0 || >=24.0.0, so your version should satisfy this requirement from Step 1.
If npx hangs, check your internet connection. The command downloads the template from the npm registry.
help me fix an error when running npx create-expo-app
Replace the app code with your test dashboard
The default project has placeholder code. You need to replace it with a user dashboard that fetches data from your mock API and exposes every element through testID props.
What is testID?
In React Native, the testID prop becomes an accessibility ID on the device. Appium uses accessibility IDs to find and interact with elements reliably. Without testID props, your tests would rely on fragile XPath selectors that break when the UI changes.
- In VS Code, open the user-dashboard folder (File > Open Folder > select user-dashboard on your Desktop).
- Open App.js from the file explorer sidebar.
- Select all the existing code (Ctrl+A) and delete it.
- Paste the following user dashboard code into the empty file:
import { useState, useEffect } from 'react';
import { View, Text, TouchableOpacity, ScrollView, StyleSheet } from 'react-native';
// Your PC's local network IP - Appium tests will hit this API
const API_URL = 'http://[[YOUR_PC_IP="192.168.x.x"]]:3000';
export default function App() {
// State for user data fetched from the mock API
const [user, setUser] = useState(null);
// State to track which screen is showing
const [screen, setScreen] = useState('dashboard');
// Local settings state for the settings screen
const [settings, setSettings] = useState({ notifications: true, darkMode: false });
// Fetch user data from the mock API on mount
useEffect(() => {
fetch(`${API_URL}/user`)
.then(res => res.json())
.then(data => setUser(data))
.catch(err => console.error(err));
}, []);
// Loading screen while waiting for API data
if (!user) {
return (
<View style={styles.container} testID="loading-screen">
<Text testID="loading-text">Loading...</Text>
</View>
);
}
// Settings screen with toggleable options
if (screen === 'settings') {
return (
<ScrollView style={styles.container} testID="settings-screen">
<Text style={styles.title} testID="settings-title">Settings</Text>
<TouchableOpacity
testID="back-button"
style={styles.button}
onPress={() => setScreen('dashboard')}
>
<Text>Back to Dashboard</Text>
</TouchableOpacity>
<TouchableOpacity
testID="toggle-notifications"
style={styles.settingItem}
onPress={() => setSettings(s => ({ ...s, notifications: !s.notifications }))}
>
<Text>Notifications: {settings.notifications ? 'ON' : 'OFF'}</Text>
</TouchableOpacity>
<TouchableOpacity
testID="toggle-darkmode"
style={styles.settingItem}
onPress={() => setSettings(s => ({ ...s, darkMode: !s.darkMode }))}
>
<Text>Dark Mode: {settings.darkMode ? 'ON' : 'OFF'}</Text>
</TouchableOpacity>
{user.role === 'admin' && (
<View testID="admin-settings">
<Text style={styles.subtitle}>Admin Options</Text>
<TouchableOpacity testID="manage-users" style={styles.settingItem}>
<Text>Manage Users</Text>
</TouchableOpacity>
</View>
)}
</ScrollView>
);
}
// Promo details screen shown when user taps the promo banner
if (screen === 'promo-details') {
return (
<View style={styles.container} testID="promo-details-screen">
<Text style={styles.title} testID="promo-details-title">
{user.promo?.title || 'No Promo'}
</Text>
<Text testID="promo-details-description">{user.promo?.description || ''}</Text>
<TouchableOpacity
testID="back-button"
style={styles.button}
onPress={() => setScreen('dashboard')}
>
<Text>Back to Dashboard</Text>
</TouchableOpacity>
</View>
);
}
// Main dashboard screen - the default view
return (
<ScrollView style={styles.container} testID="dashboard-screen">
<Text style={styles.title} testID="user-name">{user.name}</Text>
<Text testID="subscription-status">
Subscription: {user.subscription}
</Text>
{user.subscription === 'expired' && (
<TouchableOpacity testID="renew-button" style={styles.renewButton}>
<Text style={styles.renewText}>Renew Subscription</Text>
</TouchableOpacity>
)}
{user.promo && (
<TouchableOpacity
testID="promo-banner"
style={styles.promoBanner}
onPress={() => setScreen('promo-details')}
>
<Text testID="promo-title">{user.promo.title}</Text>
<Text testID="promo-discount">{user.promo.discount}% OFF</Text>
</TouchableOpacity>
)}
<TouchableOpacity
testID="settings-button"
style={styles.button}
onPress={() => setScreen('settings')}
>
<Text>Settings</Text>
</TouchableOpacity>
</ScrollView>
);
}
What does this code do?
This app has four screens controlled by a screen state variable: a loading screen, a dashboard, a settings screen, and a promo details screen.
On mount, it fetches user data from API_URL (your mock server in the next step). Every interactive element has a testID prop like user-name, settings-button, and promo-banner. Appium finds these elements using the $('~user-name') selector pattern.
Conditional rendering shows different UI based on the data. When subscription is expired, a renew button appears. When a promo exists, a banner appears. This is how your mock data will drive different test scenarios.
- Now add the StyleSheet at the bottom of the same file, below the closing brace of the App function. Paste this code:
const styles = StyleSheet.create({
container: { flex: 1, padding: 20, paddingTop: 60 },
title: { fontSize: 24, fontWeight: 'bold', marginBottom: 10 },
subtitle: { fontSize: 18, fontWeight: 'bold', marginTop: 20, marginBottom: 10 },
button: { padding: 15, backgroundColor: '#e0e0e0', borderRadius: 8, marginTop: 10 },
settingItem: { padding: 15, backgroundColor: '#f5f5f5', borderRadius: 8, marginTop: 8 },
renewButton: { padding: 15, backgroundColor: '#ff6b6b', borderRadius: 8, marginTop: 10 },
renewText: { color: 'white', fontWeight: 'bold', textAlign: 'center' },
promoBanner: { padding: 15, backgroundColor: '#4ecdc4', borderRadius: 8, marginTop: 10 },
});
What does this code do?
The StyleSheet defines visual styles for each component. The dashboard uses distinct colors for different elements: red for the renewal button, teal for promo banners, and light gray for standard buttons. This makes it easy to visually confirm your tests are tapping the right elements.
Find your PC's local IP address
You will need your PC's local network IP for API_URL so the app on your phone can reach the mock server. Open a new terminal and run ipconfig. Look for the IPv4 Address under your Wi-Fi or Ethernet adapter (something like 192.168.1.100).
Replace YOUR_PC_IP in the code with that address. Your phone must be on the same network as your PC.
- Save App.js (Ctrl+S).
✔️ Awesome, I've got everything!
Great. Double check you saved the file before moving on to the build step.
ⓧ I'd like to double check the full code
Here is the complete App.js file. Make sure your code matches this exactly:
import { useState, useEffect } from 'react';
import { View, Text, TouchableOpacity, ScrollView, StyleSheet } from 'react-native';
const API_URL = 'http://YOUR_PC_IP:3000';
export default function App() {
const [user, setUser] = useState(null);
const [screen, setScreen] = useState('dashboard');
const [settings, setSettings] = useState({ notifications: true, darkMode: false });
useEffect(() => {
fetch(`${API_URL}/user`)
.then(res => res.json())
.then(data => setUser(data))
.catch(err => console.error(err));
}, []);
if (!user) {
return (
<View style={styles.container} testID="loading-screen">
<Text testID="loading-text">Loading...</Text>
</View>
);
}
if (screen === 'settings') {
return (
<ScrollView style={styles.container} testID="settings-screen">
<Text style={styles.title} testID="settings-title">Settings</Text>
<TouchableOpacity
testID="back-button"
style={styles.button}
onPress={() => setScreen('dashboard')}
>
<Text>Back to Dashboard</Text>
</TouchableOpacity>
<TouchableOpacity
testID="toggle-notifications"
style={styles.settingItem}
onPress={() => setSettings(s => ({ ...s, notifications: !s.notifications }))}
>
<Text>Notifications: {settings.notifications ? 'ON' : 'OFF'}</Text>
</TouchableOpacity>
<TouchableOpacity
testID="toggle-darkmode"
style={styles.settingItem}
onPress={() => setSettings(s => ({ ...s, darkMode: !s.darkMode }))}
>
<Text>Dark Mode: {settings.darkMode ? 'ON' : 'OFF'}</Text>
</TouchableOpacity>
{user.role === 'admin' && (
<View testID="admin-settings">
<Text style={styles.subtitle}>Admin Options</Text>
<TouchableOpacity testID="manage-users" style={styles.settingItem}>
<Text>Manage Users</Text>
</TouchableOpacity>
</View>
)}
</ScrollView>
);
}
if (screen === 'promo-details') {
return (
<View style={styles.container} testID="promo-details-screen">
<Text style={styles.title} testID="promo-details-title">
{user.promo?.title || 'No Promo'}
</Text>
<Text testID="promo-details-description">{user.promo?.description || ''}</Text>
<TouchableOpacity
testID="back-button"
style={styles.button}
onPress={() => setScreen('dashboard')}
>
<Text>Back to Dashboard</Text>
</TouchableOpacity>
</View>
);
}
return (
<ScrollView style={styles.container} testID="dashboard-screen">
<Text style={styles.title} testID="user-name">{user.name}</Text>
<Text testID="subscription-status">
Subscription: {user.subscription}
</Text>
{user.subscription === 'expired' && (
<TouchableOpacity testID="renew-button" style={styles.renewButton}>
<Text style={styles.renewText}>Renew Subscription</Text>
</TouchableOpacity>
)}
{user.promo && (
<TouchableOpacity
testID="promo-banner"
style={styles.promoBanner}
onPress={() => setScreen('promo-details')}
>
<Text testID="promo-title">{user.promo.title}</Text>
<Text testID="promo-discount">{user.promo.discount}% OFF</Text>
</TouchableOpacity>
)}
<TouchableOpacity
testID="settings-button"
style={styles.button}
onPress={() => setScreen('settings')}
>
<Text>Settings</Text>
</TouchableOpacity>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 20, paddingTop: 60 },
title: { fontSize: 24, fontWeight: 'bold', marginBottom: 10 },
subtitle: { fontSize: 18, fontWeight: 'bold', marginTop: 20, marginBottom: 10 },
button: { padding: 15, backgroundColor: '#e0e0e0', borderRadius: 8, marginTop: 10 },
settingItem: { padding: 15, backgroundColor: '#f5f5f5', borderRadius: 8, marginTop: 8 },
renewButton: { padding: 15, backgroundColor: '#ff6b6b', borderRadius: 8, marginTop: 10 },
renewText: { color: 'white', fontWeight: 'bold', textAlign: 'center' },
promoBanner: { padding: 15, backgroundColor: '#4ecdc4', borderRadius: 8, marginTop: 10 },
});
Configure EAS Build and install the APK
With the app code ready, you need to compile it into an installable APK file. EAS Build handles the compilation in the cloud and gives you a download link.
- Configure your project for EAS Build by running:
eas build:configure
What will this command ask?
The command will prompt you to select a platform. Choose Android (since we only need an Android build). It creates an eas.json file in your project root with default build profiles.
- Open eas.json in VS Code from the file explorer sidebar.
- Find the preview profile section. It currently looks like this:
"preview": {
"distribution": "internal"
}
- Replace the preview section with this updated version that adds the APK build type:
"preview": {
"distribution": "internal",
"android": {
"buildType": "apk"
}
}
Why set buildType to apk?
By default, EAS Build produces an AAB (Android App Bundle) meant for the Play Store. Setting buildType to apk produces a standalone APK file you can install directly on your phone with adb install. This is exactly what Appium needs to automate.
- Save eas.json (Ctrl+S).
- Start the cloud build by running:
eas build --platform android --profile preview
What happens during the build?
EAS uploads your project to their cloud servers and compiles it. The free tier uses a low-priority build queue, so this can take 5-15 minutes. You will see a progress URL in your terminal. You can also check the build status at expo.dev under your project's builds.
When the build finishes, the terminal prints a download URL for your APK file.
- Wait for the build to complete (watch for the download URL in the terminal output).
- Download the APK file using the URL provided. Save it somewhere easy to find, like your Desktop.
- Install the APK on your phone by running (adjust the path to where you saved the file):
adb install [[APK_PATH="path\to\your-downloaded.apk"]]
You should see Success in the terminal output after the install completes.
- On your phone, find and tap the user-dashboard app icon to launch it.
The app will show a "Loading..." screen because the mock API server is not running yet. That is expected. You will set up the server in the next step.
Build failed or APK won't install?
If the EAS build fails, check the build logs at expo.dev for specific error messages. Common issues include missing eas.json configuration or a typo in the build profile name.
If adb install fails with a signature error, your phone may have a previous version installed. Uninstall it first from your phone's app settings, then try the install again.
help me debug my EAS Build or adb install error
Your test app is installed and ready on your phone. Next, you will set up the mock API server that feeds data to the app, letting you control exactly what your tests see on screen.
Set Up json-server as Your Mock API
Your React Native app is installed on your phone and tries to fetch user data from port 3000. Right now, nothing is running there, so the app just shows "Loading..." forever.
In this step, you'll spin up a mock API server using json-server. You'll also create multiple "scenario" files that let you instantly swap what data the app receives between test runs.
In this step, get ready to:
- Install json-server and serve default user data on port 3000.
- Create scenario files representing different app states (expired subscription, admin user, holiday promo).
- Verify your phone can reach the mock API over your local network.
Create the mock server and install json-server
Your mock server lives in its own folder, separate from the React Native app. This keeps your test data cleanly organized.
- Navigate to your Desktop (where user-dashboard already lives) by running this command:
cd ~\Desktop
- Create a new folder called mock-server and move into it by running these commands:
mkdir mock-server
cd mock-server
- Initialize a new Node.js project and install json-server by running these commands:
npm init -y
npm install json-server
What does this do?
The npm init -y command creates a package.json file that tracks your project's dependencies. The -y flag accepts all defaults so you don't have to answer prompts.
Installing json-server adds it as a local dependency. json-server takes any JSON file and instantly turns it into a full REST API with GET, POST, PUT, and DELETE endpoints.
- Confirm your folder is set up by running:
ls
- You should see node_modules, package.json, and package-lock.json in the output.
npm install failed?
If you see permission errors, make sure you're running your terminal as a regular user (not Administrator). If npm is not recognized, confirm Node.js is still in your PATH by running node --version.
help me fix this npm install error
Now you need to create the JSON file that json-server will serve as your API.
- Open the mock-server folder in VS Code. Click File in the top menu bar.
- Click Open Folder.
- Select the mock-server folder on your Desktop, then click Select Folder.
- Create a new file called db.json by pressing Ctrl+N, then Ctrl+S and saving as db.json.
- Paste the following code into db.json:
{
"user": {
"name": "John Doe",
"subscription": "active",
"role": "user",
"promo": null
}
}
What does this data do?
When json-server runs, it reads this file and creates a /user endpoint that returns this JSON object. Your React Native app's fetch call hits that endpoint and displays the data on screen.
This is the "default" state: a regular user named John Doe with an active subscription and no promo banner.
- Save db.json with Ctrl+S.
Create scenario files and a swap script
Testing edge cases (like an expired subscription or admin permissions) normally requires complex backend setup. With scenario files, you just swap one JSON file and restart the server.
- Create a scenarios folder inside mock-server by running this command in your terminal:
mkdir scenarios
- In VS Code, right-click the new scenarios folder in the Explorer sidebar and select New File.
- Name it premium-expired.json and paste the following code:
{
"user": {
"name": "John Doe",
"subscription": "expired",
"role": "user",
"promo": null
}
}
- Create another file in the scenarios folder called admin-user.json and paste:
{
"user": {
"name": "Admin Jane",
"subscription": "active",
"role": "admin",
"promo": null
}
}
- Create another file in scenarios called holiday-promo.json and paste:
{
"user": {
"name": "John Doe",
"subscription": "active",
"role": "user",
"promo": {
"title": "Holiday Special",
"discount": 50,
"description": "Get 50% off your annual subscription this holiday season!"
}
}
}
- Create one more file in scenarios called default.json and paste:
{
"user": {
"name": "John Doe",
"subscription": "active",
"role": "user",
"promo": null
}
}
What do these scenarios represent?
- premium-expired.json: A user whose subscription has lapsed. This triggers the "Renew" button in the app.
- admin-user.json: An admin user who sees extra settings options.
- holiday-promo.json: A user with an active promo banner offering 50% off.
- default.json: The baseline state. Identical to your db.json so you can reset back.
- Save all four files.
Now you need a script that copies the right scenario file into db.json so you can switch states with a single command.
- Create a new file in the mock-server root folder (not inside scenarios) called swap-scenario.js and paste:
const fs = require('fs');
const path = require('path');
// Read the scenario name from the command line argument
const scenario = process.argv[2];
if (!scenario) {
console.log('Usage: node swap-scenario.js <scenario-name>');
console.log('Available: default, premium-expired, admin-user, holiday-promo');
process.exit(1);
}
// Build the path to the matching scenario file
const scenarioFile = scenario === 'default'
? path.join(__dirname, 'scenarios', 'default.json')
: path.join(__dirname, 'scenarios', `${scenario}.json`);
// Check the scenario file exists before copying
if (!fs.existsSync(scenarioFile)) {
console.error(`Scenario file not found: ${scenarioFile}`);
process.exit(1);
}
// Overwrite db.json with the scenario data
fs.copyFileSync(scenarioFile, path.join(__dirname, 'db.json'));
console.log(`Switched to scenario: ${scenario}`);
What does this script do?
When you run node swap-scenario.js premium-expired, the script copies scenarios/premium-expired.json over db.json. The next time json-server starts (or you restart it), it serves the expired-subscription data instead.
This means your automated tests can call this script before each test run to set up any scenario they need.
- Save swap-scenario.js.
- Test the swap script by running this in your terminal:
node swap-scenario.js premium-expired
- You should see Switched to scenario: premium-expired printed in the terminal.
- Reset back to the default data by running:
node swap-scenario.js default
Seeing 'Scenario file not found'?
Make sure you created the scenario files inside the scenarios folder (not in the mock-server root). Check the filenames match exactly: premium-expired.json, admin-user.json, holiday-promo.json, default.json.
help me fix the swap-scenario script
\u2714\ufe0f Awesome, I've got everything!
Great. Double check you've saved all files, then move on to starting the server.
\u24e7 I'd like to double check the full code
Here is the complete swap-scenario.js file:
const fs = require('fs');
const path = require('path');
const scenario = process.argv[2];
if (!scenario) {
console.log('Usage: node swap-scenario.js <scenario-name>');
console.log('Available: default, premium-expired, admin-user, holiday-promo');
process.exit(1);
}
const scenarioFile = scenario === 'default'
? path.join(__dirname, 'scenarios', 'default.json')
: path.join(__dirname, 'scenarios', `${scenario}.json`);
if (!fs.existsSync(scenarioFile)) {
console.error(`Scenario file not found: ${scenarioFile}`);
process.exit(1);
}
fs.copyFileSync(scenarioFile, path.join(__dirname, 'db.json'));
console.log(`Switched to scenario: ${scenario}`);
And here is db.json (reset to default):
{
"user": {
"name": "John Doe",
"subscription": "active",
"role": "user",
"promo": null
}
}
Start json-server and verify phone connectivity
Your phone and PC need to be on the same Wi-Fi network for the app to reach the mock server. The --host 0.0.0.0 flag tells json-server to accept connections from any device on your network, not just your PC.
- Find your PC's local IP address by running this command:
ipconfig
- Look for the IPv4 Address line under your Wi-Fi adapter. It will look something like 192.168.1.105.
- Write this IP address down. You will use it in the next step.
Why do I need my PC's IP address?
Your phone cannot use localhost to reach your PC. It needs your PC's actual network IP address to connect over Wi-Fi. This is the same IP your app already references in API_URL.
- Start json-server by running this command (make sure you're inside the mock-server folder):
npx json-server db.json --port 3000 --host 0.0.0.0
- You should see output showing the server is running and listing the available endpoints including /user.
What does this command do?
- npx json-server runs the locally installed json-server package.
- db.json tells it which file to serve as the API data.
- --port 3000 sets the port number (matching what your app expects).
- --host 0.0.0.0 makes the server accessible from other devices on your network, not just your PC.
- On your Android phone, open the Chrome browser.
- Type your PC's IP address followed by the port and endpoint into the address bar. For example: http://192.168.1.105:3000/user (replace with your actual IP).
- You should see the JSON data from db.json displayed in the browser: John Doe, active subscription, no promo.
Phone can't reach the server?
Make sure your phone and PC are on the same Wi-Fi network. If you're using a VPN on either device, try disabling it. Windows Firewall may also block incoming connections on port 3000. You can temporarily allow it through Windows Defender Firewall by adding an inbound rule for port 3000.
help me connect my phone to json-server
Now test that scenario swapping works end-to-end.
- Stop the server by pressing Ctrl+C in your terminal.
- Swap to the holiday promo scenario by running:
node swap-scenario.js holiday-promo
- Restart the server by running:
npx json-server db.json --port 3000 --host 0.0.0.0
- Refresh the page on your phone browser. You should now see the holiday promo data with the title "Holiday Special" and discount of 50.
- Stop the server again with Ctrl+C, then reset to default:
node swap-scenario.js default
npx json-server db.json --port 3000 --host 0.0.0.0
- Refresh your phone browser one more time to confirm the data shows John Doe with an active subscription again.
Data not changing after swap?
json-server reads db.json at startup. You must stop the server with Ctrl+C and restart it after running swap-scenario.js for the new data to take effect.
help me debug why my mock data isn't changing
Your mock API is running and your phone can reach it. Next up, you'll write automated tests that launch the app and interact with it using Appium.
Write Your First Appium Test with WebdriverIO
Your app is installed on the phone and your mock server is feeding it data. Now it is time to write code that controls the phone remotely.
In this step you will use WebdriverIO to send commands to your phone through Appium. WebdriverIO finds elements by their accessibility ID (the testID you added in the React Native app), taps them, reads their text, and asserts results.
In this step, get ready to:
- Initialize a WebdriverIO test project with Appium support.
- Write test specs that find elements by accessibility ID and assert their text.
- Run the tests and watch your phone execute the actions automatically.
Create and configure your WebdriverIO test project
WebdriverIO provides a setup wizard that scaffolds a complete test project. It installs all necessary packages and generates a config file based on your choices.
- Open a new terminal window. Keep your json-server terminal running separately.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create the test project folder and move into it by running these commands:
mkdir appium-tests
cd appium-tests
- Copy the APK file you downloaded from EAS Build into this folder. Rename it to app-release.apk so your test config can find it.
- Initialize the WebdriverIO project by running this command:
npm init wdio@latest ./
What does this wizard ask?
The WebdriverIO setup wizard asks a series of questions to configure your project. It will ask about your runner, framework, compiler, where to put test files, reporters, and services. Follow the selections below to match this project's needs.
- When the wizard asks which runner to use, select local.
- When it asks which framework to use, select Mocha.
- When it asks about a compiler or transpiler, select No (no compiler).
- When it asks about services, select appium.
- Accept the defaults for all other prompts (test file location, reporters, base URL).
- When it asks to run npm install, confirm yes.
The wizard installs all packages and generates a wdio.conf.js file. You need to replace the capabilities section with your Android device settings.
- Find your device serial number by running:
adb devices
You should see your device serial number listed (something like R5CR1234567). Copy this value.
- Open wdio.conf.js in VS Code and replace its entire contents with the following configuration:
exports.config = {
// Use local runner to execute tests on this machine
runner: 'local',
// Appium server port
port: 4723,
// Find all test files in test/specs/
specs: ['./test/specs/**/*.js'],
maxInstances: 1,
capabilities: [{
platformName: 'Android',
'appium:automationName': 'UiAutomator2',
'appium:deviceName': '[[DEVICE_SERIAL="your device serial from adb devices"]]',
'appium:app': './app-release.apk',
'appium:noReset': false,
'appium:fullReset': false,
'appium:newCommandTimeout': 240
}],
// WebdriverIO starts Appium automatically
services: ['appium'],
framework: 'mocha',
reporters: ['spec'],
mochaOpts: {
ui: 'bdd',
// 60 seconds per test before timeout
timeout: 60000
}
};
What does this configuration do?
This file tells WebdriverIO how to connect to your phone and run tests.
- platformName: 'Android' targets the Android operating system on your phone.
- 'appium:automationName': 'UiAutomator2' tells Appium to use the UiAutomator2 driver, which controls Android UI elements.
- 'appium:app': './app-release.apk' points to your APK file so Appium can install and launch it.
- services: ['appium'] makes WebdriverIO automatically start an Appium server before tests run.
- timeout: 60000 gives each test 60 seconds to complete, which is generous for mobile automation.
- Save wdio.conf.js.
Not sure where to find your APK?
The APK was downloaded from the EAS Build dashboard in Step 2. If you cannot find the file, return to expo.dev, open your build, and download it again. Place it in the appium-tests folder and name it app-release.apk.
Help me find my APK file from the EAS Build dashboard
Write the dashboard test spec
Each test spec is a JavaScript file that describes a set of interactions and assertions. Your first spec will verify that the app displays the correct user data from the mock server.
- Create the test directory structure by running these commands:
mkdir -p test/specs
- Confirm the folder was created by running:
ls test/specs
You should see the empty specs directory listed without errors.
- Create a new file called dashboard.spec.js inside test/specs/ and paste the following code:
describe('User Dashboard', () => {
it('should display the user name from mock data', async () => {
// Find the element by its accessibility ID (maps to testID in React Native)
const userName = await $('~user-name');
// Wait up to 10 seconds for the app to load data from json-server
await userName.waitForDisplayed({ timeout: 10000 });
// Read the displayed text and compare to mock data
const text = await userName.getText();
expect(text).toBe('John Doe');
});
it('should display active subscription status', async () => {
// Find the subscription status element
const status = await $('~subscription-status');
await status.waitForDisplayed({ timeout: 5000 });
// Verify the status text contains the expected value from db.json
const text = await status.getText();
expect(text).toContain('active');
});
it('should navigate to settings screen', async () => {
// Find and tap the settings button
const settingsButton = await $('~settings-button');
await settingsButton.click();
// Verify the settings screen appeared after navigation
const settingsTitle = await $('~settings-title');
await settingsTitle.waitForDisplayed({ timeout: 5000 });
expect(await settingsTitle.getText()).toBe('Settings');
});
});
How does the accessibility ID selector work?
The $('~user-name') syntax uses the ~ prefix to find elements by their accessibility ID. This maps directly to the testID prop you set in your React Native app.
The first test verifies the user name matches the mock data in db.json. The second checks subscription status. The third taps a button and asserts that navigation worked by checking the new screen appeared.
- Save dashboard.spec.js.
✔️ Awesome, I've got everything!
Great. Double check you have saved both wdio.conf.js and dashboard.spec.js.
ⓧ I'd like to double check the full code
Here is the complete wdio.conf.js:
exports.config = {
runner: 'local',
port: 4723,
specs: ['./test/specs/**/*.js'],
maxInstances: 1,
capabilities: [{
platformName: 'Android',
'appium:automationName': 'UiAutomator2',
'appium:deviceName': 'your-device-serial',
'appium:app': './app-release.apk',
'appium:noReset': false,
'appium:fullReset': false,
'appium:newCommandTimeout': 240
}],
services: ['appium'],
framework: 'mocha',
reporters: ['spec'],
mochaOpts: {
ui: 'bdd',
timeout: 60000
}
};
Here is the complete dashboard.spec.js:
describe('User Dashboard', () => {
it('should display the user name from mock data', async () => {
const userName = await $('~user-name');
await userName.waitForDisplayed({ timeout: 10000 });
const text = await userName.getText();
expect(text).toBe('John Doe');
});
it('should display active subscription status', async () => {
const status = await $('~subscription-status');
await status.waitForDisplayed({ timeout: 5000 });
const text = await status.getText();
expect(text).toContain('active');
});
it('should navigate to settings screen', async () => {
const settingsButton = await $('~settings-button');
await settingsButton.click();
const settingsTitle = await $('~settings-title');
await settingsTitle.waitForDisplayed({ timeout: 5000 });
expect(await settingsTitle.getText()).toBe('Settings');
});
});
Run the tests and watch your phone automate
With the config and test spec in place, you can now run your automation suite. WebdriverIO will start Appium, install the app on your phone, and execute each test.
- Make sure your phone is still connected via USB. Confirm by running:
adb devices
- Make sure json-server is still running in the other terminal. If it stopped, switch back to that terminal and restart it.
- Run the test suite by running this command:
npx wdio run wdio.conf.js
Watch your phone. You should see the app launch automatically, the dashboard load, and the settings screen open as the test taps the button.
In the terminal, you should see three passing tests with green checkmarks and the test names printed by the spec reporter.
What just happened?
WebdriverIO started an Appium server on port 4723. Appium used the UiAutomator2 driver to communicate with your phone over USB. Each test sent commands through the WebDriver protocol to find elements, read text, and tap buttons.
Your phone responded exactly as if a human was using it. The entire flow happened in seconds.
Tests failing or phone not responding?
- If you see a timeout error, make sure your phone is unlocked and the screen is on. Appium cannot interact with a locked device.
- If you see a connection error, verify adb devices still shows your device. Try unplugging and replugging the USB cable.
- If the app loads but tests time out waiting for elements, make sure json-server is running. The app needs to fetch data from the mock API to display anything.
- If you see a device not found error, check that the appium:deviceName in wdio.conf.js matches the serial number shown by adb devices.
Help me debug my failing Appium test on a physical Android device
Your tests are passing and your phone is doing exactly what the code tells it to. Next up, you will swap mock data between test runs to simulate impossible scenarios like expired subscriptions and holiday promotions.
Mock Impossible Scenarios and Assert Results
In the previous step, you proved your automation setup works by running a test that taps through your app and verifies text on screen. That test confirmed the default "active subscription" state.
Now you will test states that would take days to reproduce manually. What does a user see when their premium subscription expires? What happens when a holiday promo appears? By swapping your mock data before each test, you simulate these scenarios in seconds.
In this step, get ready to:
- Write a test for the "premium expired" scenario that asserts renewal UI appears.
- Write a test for the "holiday promo" scenario that verifies banner content and navigation.
- Run all scenario tests in sequence using your swap helper.
Write the premium expired scenario test
Imagine a user whose subscription expired 90 days ago. In production, you would need to wait 90 real days or manually hack a database to see this state. With mock data, you just swap a JSON file.
- First, swap your mock server to the expired scenario. In your terminal that is inside the mock-server folder, run:
node swap-scenario.js premium-expired
- You should see Switched to scenario: premium-expired in the terminal output.
- Restart json-server so it picks up the new data. Stop the running server with Ctrl+C, then start it again by running:
npx json-server db.json --port 3000 --host 0.0.0.0
- Now create the test file. In VS Code, create a new file at appium-tests/test/specs/premium-expired.spec.js and paste the following code:
describe('Premium Expired Scenario', () => {
it('should show expired subscription status', async () => {
// Find the subscription status element by accessibility ID
const status = await $('~subscription-status');
await status.waitForDisplayed({ timeout: 10000 });
// Verify the text contains "expired" from our mock data
const text = await status.getText();
expect(text).toContain('expired');
});
it('should display the renew button', async () => {
// The renew button only appears when subscription is expired
const renewButton = await $('~renew-button');
await renewButton.waitForDisplayed({ timeout: 5000 });
expect(await renewButton.isDisplayed()).toBe(true);
});
});
What does this test verify?
This test checks two things that only appear when a subscription is expired.
- The ~subscription-status element displays "expired" instead of "active".
- The ~renew-button element is visible on screen. In your app code, this button only renders when subscription === 'expired'.
- Save premium-expired.spec.js.
- Open a separate terminal inside the appium-tests folder and run the test by executing:
npx wdio run wdio.conf.js --spec ./test/specs/premium-expired.spec.js
- Watch your phone. The app launches, and your test checks the expired status and verifies the renew button appears. The terminal shows green checkmarks for both passing assertions.
Test says element not found?
- Make sure you restarted json-server after running the swap script. The server reads db.json at startup, so changes to the file require a restart.
- Confirm the swap worked by visiting http://YOUR_PC_IP:3000/user in your phone browser. You should see "subscription": "expired" in the response.
- help me debug why my premium-expired test cannot find the renew button element
Write the holiday promo scenario test
Holiday promotions only appear during specific marketing campaigns. Testing promo banner visibility, discount values, and navigation normally requires coordinating with a marketing team. Your mock data makes this instant.
- Switch to the holiday-promo scenario. In your mock-server terminal, stop json-server with Ctrl+C, then run:
node swap-scenario.js holiday-promo
npx json-server db.json --port 3000 --host 0.0.0.0
- Create a new file at appium-tests/test/specs/holiday-promo.spec.js and paste this code:
describe('Holiday Promo Scenario', () => {
it('should display the promo banner', async () => {
// Promo banner only renders when user.promo is not null
const promoBanner = await $('~promo-banner');
await promoBanner.waitForDisplayed({ timeout: 10000 });
expect(await promoBanner.isDisplayed()).toBe(true);
});
it('should show correct discount percentage', async () => {
// Verify the discount value matches our mock data (50%)
const discount = await $('~promo-discount');
await discount.waitForDisplayed({ timeout: 5000 });
const text = await discount.getText();
expect(text).toContain('50');
});
it('should navigate to promo details on tap', async () => {
// Tap the promo banner and verify the details screen appears
const promoBanner = await $('~promo-banner');
await promoBanner.click();
const detailsScreen = await $('~promo-details-screen');
await detailsScreen.waitForDisplayed({ timeout: 5000 });
expect(await detailsScreen.isDisplayed()).toBe(true);
});
});
What does this test verify?
This test covers three distinct aspects of the promo experience.
- The promo banner element (~promo-banner) is visible when promo data exists.
- The discount percentage displays "50" as set in the mock data.
- Tapping the banner navigates to the promo details screen, proving the interaction works end-to-end.
- Save holiday-promo.spec.js.
- In your appium-tests terminal, run the holiday promo test by executing:
npx wdio run wdio.conf.js --spec ./test/specs/holiday-promo.spec.js
- Watch your phone. The app opens, the promo banner appears with "50% OFF", your test taps it, and the promo details screen loads. Three green checkmarks appear in the terminal.
Promo banner test timing out?
- Verify you ran node swap-scenario.js holiday-promo and restarted json-server before running the test.
- Check that your phone can still reach the API. Open your phone browser and visit your PC's IP on port 3000. The JSON should show a promo object with "title": "Holiday Special".
- help me figure out why my holiday promo test is timing out
Run all scenarios in sequence
You have been swapping scenarios manually before each test run. The swap-scenario.js script you built earlier is your test helper. It copies the right JSON file into db.json so json-server serves the correct data for each test suite.
Why swap data manually instead of programmatically inside tests?
Keeping the scenario swap outside your test files means each spec is self-contained and focused on assertions only. Your tests do not need to know about file system operations or server restarts. This separation makes tests easier to read, debug, and reuse.
- Run all three scenario tests in sequence. In your mock-server terminal, stop json-server with Ctrl+C, then swap back to the default scenario and restart:
node swap-scenario.js default
npx json-server db.json --port 3000 --host 0.0.0.0
- In your appium-tests terminal, run the dashboard spec first by executing:
npx wdio run wdio.conf.js --spec ./test/specs/dashboard.spec.js
- Once the dashboard tests pass, swap to the premium-expired scenario. In your mock-server terminal, stop json-server with Ctrl+C and run:
node swap-scenario.js premium-expired
npx json-server db.json --port 3000 --host 0.0.0.0
- In your appium-tests terminal, run the premium-expired spec:
npx wdio run wdio.conf.js --spec ./test/specs/premium-expired.spec.js
- Finally, swap to holiday-promo. Stop json-server with Ctrl+C and run:
node swap-scenario.js holiday-promo
npx json-server db.json --port 3000 --host 0.0.0.0
- In your appium-tests terminal, run the holiday-promo spec:
npx wdio run wdio.conf.js --spec ./test/specs/holiday-promo.spec.js
- All three test suites pass with green checkmarks. You have now tested three completely different app states by swapping a single JSON file each time.
What just happened?
You tested an expired subscription, a holiday promo campaign, and the default active state. Each scenario would require different backend configurations, time-based triggers, or admin access in a real production environment.
With your mock server, you achieved all three in under a minute. This is the core value of data-driven testing. The same app, the same tests, completely different outcomes controlled by a single data file.
One scenario passes but another fails?
- Double-check you swapped to the correct scenario before each run. Run node swap-scenario.js with the matching scenario name.
- Make sure json-server was restarted after every swap. It reads the file once at startup.
- If a test passes one time but fails the next, check that your phone is not caching old app data. The appium:noReset: false capability in your wdio.conf.js should clear app state between runs.
- help me figure out why one scenario test passes but another fails
✔️ Awesome, I've got everything!
Great. Double check that you have saved both premium-expired.spec.js and holiday-promo.spec.js.
ⓧ I'd like to double check the full code
Here is the complete code for both new test spec files.
describe('Premium Expired Scenario', () => {
it('should show expired subscription status', async () => {
const status = await $('~subscription-status');
await status.waitForDisplayed({ timeout: 10000 });
const text = await status.getText();
expect(text).toContain('expired');
});
it('should display the renew button', async () => {
const renewButton = await $('~renew-button');
await renewButton.waitForDisplayed({ timeout: 5000 });
expect(await renewButton.isDisplayed()).toBe(true);
});
});
describe('Holiday Promo Scenario', () => {
it('should display the promo banner', async () => {
const promoBanner = await $('~promo-banner');
await promoBanner.waitForDisplayed({ timeout: 10000 });
expect(await promoBanner.isDisplayed()).toBe(true);
});
it('should show correct discount percentage', async () => {
const discount = await $('~promo-discount');
await discount.waitForDisplayed({ timeout: 5000 });
const text = await discount.getText();
expect(text).toContain('50');
});
it('should navigate to promo details on tap', async () => {
const promoBanner = await $('~promo-banner');
await promoBanner.click();
const detailsScreen = await $('~promo-details-screen');
await detailsScreen.waitForDisplayed({ timeout: 5000 });
expect(await detailsScreen.isDisplayed()).toBe(true);
});
});
You have proven that mock data is your secret weapon for testing impossible scenarios instantly. Ready for an extra challenge? The Secret Mission takes this further with scroll gestures and multi-screen navigation flows.
Secret mission
Automate a Multi-Step Flow with Gestures
Your tests so far automate single-screen interactions. Real-world apps require navigating across multiple screens, scrolling through long lists, and verifying that changes on one screen reflect on another. In this mission, you'll add scroll gestures and multi-screen assertions to your automation toolkit.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your tools installed, pause the running server, or delete everything entirely.
Resources you used:
- Android SDK Platform-Tools at C:\Android\platform-tools.
- Appium and EAS CLI (global npm packages).
- User dashboard APK on your Android phone.
- Local project folders (user-dashboard, mock-server, appium-tests).
- Expo account (free tier).
Keep everything running
No action needed. All tools remain installed and ready for your next automation project. There are no ongoing costs since everything runs locally.
- json-server only runs when you start it manually.
- Your Expo account remains on the free tier with no charges.
- The APK stays on your phone until you manually uninstall it.
- Android SDK Platform-Tools and Appium remain ready for future projects.
Pause - I'll come back to this later
Stop json-server to free up the terminal. Everything else is dormant until you run it again.
- Press Ctrl+C in the terminal where json-server is running.
- Disconnect your phone from USB.
- When you're ready to resume, reconnect your phone, run adb devices to confirm the connection, and restart json-server.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- Uninstall the APK from your phone by running this command:
adb uninstall [[APP_PACKAGE="com.yourusername.userdashboard"]]
- Uninstall the Appium UiAutomator2 driver by running:
appium driver uninstall uiautomator2
- Uninstall Appium and EAS CLI by running:
npm uninstall -g appium eas-cli
- Delete your local project folders by running:
cd %USERPROFILE%\Desktop
rmdir /s /q user-dashboard
rmdir /s /q mock-server
rmdir /s /q appium-tests
- Delete the Android SDK Platform-Tools folder by running:
rmdir /s /q C:\Android\platform-tools
- Remove C:\Android\platform-tools from your Windows PATH. Open Settings, search for Environment Variables, open Edit the system environment variables, click Environment Variables, select Path under User variables, click Edit, and remove the C:\Android\platform-tools entry.
- To delete your Expo account, log in at expo.dev, go to your account settings, and look for the delete account option at the bottom of the page.
That's a Wrap!
That's a Wrap!
Nice work! You just built a complete mobile test automation setup that controls a real Android phone through code and simulates backend scenarios that would be impossible to reproduce manually.
You've learned how to:
- Set up a mobile automation environment on Windows with Appium, WebdriverIO, and a physical Android device connected over USB.
- Build a React Native app with testID attributes for reliable element selection and a mock API server that lets you swap backend state instantly.
- Write automated tests that verify "impossible" scenarios (expired subscriptions, holiday promos, admin permissions) by swapping mock data between runs.
- Secret Mission: Automated a multi-step navigation flow with scroll gestures and cross-screen assertions.
Ready to quiz yourself?