Improve your Mobile Automation Framework with Reporting, Parallel Execution and more— Appium 2.x, WebDriverIO and Typescript

Rochelle Abeywickrama
8 min read2 days ago

--

In this guide, I’ll be focusing on improvements for the Mobile Automation framework built in my previous guide. If you haven’t read, please do :)

Here’s how to build your own automation framework for Mobile test automation : link to guide

Now, let’s look at the additional improvements you can make on your project..

1. Install dotenv to separate environment variables:

  • Run command to install
npm install dotenv
  • Add Environment configurations to .env file to the root directory:
# Emulator
ANDROID_DEVICE_NAME=emulator-5554
ANDROID_PLATFORM_VERSION=14

# Simulator
IOS_DEVICE_NAME=iPhone 15
IOS_PLATFORM_VERSION=17.2

Some limitations / common issues on Windows vs Mac:

Resolutions for above:

  • Call dotenv.config() at the Start
#Make sure to load dotenv at the beginning of your wdio.conf.ts or entry script:
import dotenv from 'dotenv';
dotenv.config();
  • Use cross-env for Cross-Platform Compatibility
#Instead of setting environment variables differently for Windows and macOS/Linux, use cross-env:
npm install --save-dev cross-env

2. Separate capabilities according to Platform & OS:

Modify your WebDriverIO configuration file (wdio.conf.ts) to include capabilities:

— Android Native App and Chrome Browser

— iOS Native App and iOS Safari Browser

  • Create new capabilities.ts in root directory (or you can maintain a separate folder for Configs) and add the following:
export const androidAppiumConfig = {
platformName: "Android",
"appium:deviceName": process.env.ANDROID_DEVICE_NAME || "emulator-5554",
"appium:platformVersion": process.env.ANDROID_PLATFORM_VERSION || "14",
"appium:automationName": "UiAutomator2",
"appium:app": "src/app/app.apk",
"appium:autoGrantPermissions": true,
"appium:chromedriverAutodownload": true,
"appium:ignoreHiddenApiPolicyError": true
};

export const androidChromeConfig = {
platformName: "Android",
"appium:deviceName": process.env.ANDROID_DEVICE_NAME || "emulator-5554",
"appium:platformVersion": process.env.ANDROID_PLATFORM_VERSION || "14",
"appium:automationName": "UiAutomator2",
browserName: "Chrome",
"appium:chromedriverAutodownload": true,
"appium:chromedriverExecutable": "chromedriver-mobile/chromedriver"
};

export const iosAppiumConfig = {
platformName: "iOS",
"appium:deviceName": process.env.IOS_DEVICE_NAME || "iPhone 15",
"appium:platformVersion": process.env.IOS_PLATFORM_VERSION || "17.2",
"appium:automationName": "XCUITest",
"appium:app": "src/app/app.ipa",
"appium:autoGrantPermissions": true,
"appium:showXcodeLog": true,
"appium:wdaLaunchTimeout": 60000,
"appium:wdaStartupRetries": 2,
};

export const iosSafariConfig = {
platformName: "iOS",
"appium:deviceName": process.env.IOS_DEVICE_NAME || "iPhone 15",
"appium:platformVersion": process.env.IOS_PLATFORM_VERSION || "17.2",
"appium:automationName": "XCUITest",
browserName: "Safari",
"appium:showXcodeLog": true,
"appium:wdaLaunchTimeout": 60000,
"appium:wdaStartupRetries": 2,
};
  • Modify your WebDriverIO configuration file (wdio.conf.ts) to use the above configurations:
import { androidAppiumConfig, androidChromeConfig, iosAppiumConfig, iosSafariConfig } from "./capabilities";

const selectedCapability =
process.env.TEST_ENV === "app" && process.env.PLATFORM === "android" ? androidAppiumConfig :
process.env.TEST_ENV === "browser" && process.env.PLATFORM === "android" ? androidChromeConfig :
process.env.TEST_ENV === "app" && process.env.PLATFORM === "ios" ? iosAppiumConfig :
process.env.TEST_ENV === "browser" && process.env.PLATFORM === "ios" ? iosSafariConfig :
androidChromeConfig; // Default to Android Chrome if no matching condition



export const config: WebdriverIO.Config = {
capabilities: [selectedCapability],
}
  • Update LoginPage.ts so it can handle both Chrome and Native App flows:
class LoginPage {

async open() {
if (process.env.TEST_ENV === "app") {
// If testing the app, it will be launched via Appium
console.log("Running tests in native app mode");
} else {
// If testing in Chrome, open URL
await browser.url('https://example.com/login');
}
}

export default new LoginPage();
  • Modify your test file test.e2e.ts to support both Chrome and the native app.
import { expect } from '@wdio/globals';
import LoginPage from '../pageobjects/login.page';
import SecurePage from '../pageobjects/secure.page';

describe('My Login application', () => {
it('should login with valid credentials', async () => {
await LoginPage.open();

await LoginPage.login('tomsmith', 'SuperSecretPassword!');

// Verify login success
await expect(SecurePage.flashAlert).toBeExisting();
await expect(SecurePage.flashAlert).toHaveTextContaining('You logged into a secure area!');
});
});
  • Modify test run command aligning to above config changes:
# Example : Android Native App test run
npx cross-env TEST_ENV=app PLATFORM=android npx wdio run wdio.conf.ts

# Example : iOS Safari test run
npx cross-env TEST_ENV=browser PLATFORM=ios npx wdio run wdio.conf.ts

3. Externalize Testdata

  • Create a Fixture File testData.json inside your test project (e.g., ./fixtures/testData.json):
{
"login": {
"username": "TestUser1",
"password": "Testuser@123"
}
}
  • And import the JSON data to your test layer:
// Import test data from the fixture JSON
import * as testData from '../../fixtures/testData.json';

describe('User login to application', async () => {
it('User should be able to login to app successfully', async () => {
await LoginPage.open();

// Use data from the JSON file
await LoginPage.login(testData.login.username, testData.login.password);

});
});

Bonus: Using Multiple Data Sets (for Data-Driven Testing)

  • If you have multiple test scenarios, modify testData.json like this:
{
"users": [
{
"username": "TestUser1",
"password": "Password@123"
},
{
"username": "TestUser2",
"password": "Password@456"
}
]
}
  • Modify your test to run for multiple users:
testData.users.forEach((user) => {
it(`User ${user.username} should be able to place a single bet`, async () => {
await LoginPage.open();
await LoginPage.login(user.username, user.password);
});
});

4. Integrate Allure reporting

  • Install Required Dependencies
#Run the following command in your project root:
npm install --save-dev allure-commandline @wdio/allure-reporter


#This will install
# - allure-commandline: CLI tool to generate reports.
# - @wdio/allure-reporter: WebDriverIO plugin for Allure.
  • Modify your wdio.conf.ts file to include the Allure reporter:
#Add this on top to imports
import allureReporter from "@wdio/allure-reporter";

#Find the reporters section and update it like this:
reporters: [
'spec',
['allure', {
outputDir: 'allure-results', // Folder where raw allure results will be stored
disableWebdriverStepsReporting: false, // Set to true if you don't want to log WebDriver steps
disableWebdriverScreenshotsReporting: false, // Set to false to include screenshots
useCucumberStepReporter: false, // Set to true if using Cucumber
}]
],
  • Add Allure hook — Modify the onComplete hook in wdio.conf.ts
onComplete: function () {
const reportError = new Error('Could not generate Allure report');
const { exec } = require('child_process');
exec('npx allure generate allure-results --clean -o allure-report', (error: any) => {
if (error) {
console.error(reportError);
} else {
console.log('Allure report successfully generated');
}
});
}
  • Capture screenshots on failure — Modify afterTest hook in wdio.conf.ts to take screenshots:
afterTest: async function (test, context, { error }) {
if (error) {
await browser.takeScreenshot();
allureReporter.addAttachment('Screenshot', await browser.takeScreenshot(), 'image/png');
}
}
  • Adding Allure Annotations to your functions / tests
public async login (username: string, password: string) {
allureReporter.addStep("Login to application")
await this.inputUsername.setValue(username);
await this.inputPassword.setValue(password);
await this.btnSubmit.click();
}

You can use according to your desire..

  • Run Tests and Generate Allure Report
#Execute tests
npx wdio wdio.conf.ts

#Build report
npx allure generate allure-results --clean -o allure-report

#Open report
npx allure open allure-report

Optional: Install Allure for Global Use

  • To avoid running npx every time;
npm install -g allure-commandline

#Now you can directly user
allure generate allure-results --clean -o allure-report
allure open allure-report
  • You can also combine both commands together, and run:
allure generate allure-results --clean -o allure-report && allure open allure-report

This will generate the report and open it in one go!

5. Set Up Multiple Devices

  • First, connect multiple devices/emulators and ensure they are listed with:
adb devices

# Example output
List of devices attached
emulator-5554 device
emulator-5556 device
RZ8M92LZN5P device

#Each device has a unique UDID
  • Run Multiple Appium Servers (One Per Device) — since Appium does not handle multiple devices on the same server instance well, run separate Appium servers on different ports
appium -p 4723 --relaxed-security &
appium -p 4725 --relaxed-security &

# For Appium 2.0
appium --port 4723 --allow-insecure adb_shell &
appium --port 4725 --allow-insecure adb_shell &

# If you have more than two devices
appium -p 4727 &
  • Modify your WebdriverIO config (wdio.conf.ts) to run multiple sessions (for 2 devices) — here both devices are hard coded
export const config: WebdriverIO.Config = {
runner: 'local',
port: 4723, // Default Appium port
specs: ['./test/specs/**/*.ts'], // Path to test specs
maxInstances: 2, // Run two tests in parallel

capabilities: [
{
platformName: 'Android',
'appium:deviceName': 'emulator-5554',
'appium:udid': 'emulator-5554', // First device
'appium:platformVersion': '13.0',
'appium:automationName': 'UiAutomator2',
'appium:app': '/path/to/app.apk',
'appium:noReset': true,
'appium:newCommandTimeout': 240,
},
{
platformName: 'Android',
'appium:deviceName': 'emulator-5556',
'appium:udid': 'emulator-5556', // Second device
'appium:platformVersion': '12.0',
'appium:automationName': 'UiAutomator2',
'appium:app': '/path/to/app.apk',
'appium:noReset': true,
'appium:newCommandTimeout': 240,
}
],

services: ['appium'], // Enable Appium service
framework: 'mocha',
reporters: ['spec'],
};

Since capabilities are managed separately, to run tests on multiple devices in parallel, you need to modify selectedCapability to allow an array of capabilities instead of a single one.

Right now, selectedCapability only picks one capability. Instead, update it to handle multiple devices.

  • Update your wdio.config.ts like this:
const selectedCapabilities = process.env.TEST_ENV === "app" && process.env.PLATFORM === "android" 
? [androidAppiumConfig1, androidAppiumConfig2] // Add multiple devices
: process.env.TEST_ENV === "browser" && process.env.PLATFORM === "android"
? [androidChromeConfig1, androidChromeConfig2]
: process.env.TEST_ENV === "app" && process.env.PLATFORM === "ios"
? [iosAppiumConfig1, iosAppiumConfig2]
: process.env.TEST_ENV === "browser" && process.env.PLATFORM === "ios"
? [iosSafariConfig1, iosSafariConfig2]
: [androidChromeConfig1, androidChromeConfig2]; // Default to multiple Android Chrome instances

export const config: WebdriverIO.Config = {
capabilities: selectedCapabilities, // Use an array of capabilities
maxInstances: selectedCapabilities.length, // Set max instances dynamically
};
  • Modify Your capabilities.ts to include Multiple Devices
export const androidAppiumConfig1 = {
platformName: 'Android',
'appium:deviceName': 'emulator-5554',
'appium:udid': 'emulator-5554',
'appium:platformVersion': '13.0',
'appium:automationName': 'UiAutomator2',
'appium:app': '/path/to/app.apk',
'appium:noReset': true,
};

export const androidAppiumConfig2 = {
platformName: 'Android',
'appium:deviceName': 'emulator-5556',
'appium:udid': 'emulator-5556',
'appium:platformVersion': '12.0',
'appium:automationName': 'UiAutomator2',
'appium:app': '/path/to/app.apk',
'appium:noReset': true,
};
  • Start Multiple Appium Servers — Since you’re testing multiple devices, each needs its own Appium server
appium -p 4723 - relaxed-security - allow-insecure adb_shell &
appium -p 4725 - relaxed-security - allow-insecure adb_shell &

#Run the Tests in Parallel
TEST_ENV=app PLATFORM=android npx wdio run src/config/wdio.conf.ts

To support multiple Android devices with dynamic environment variables, update your capability setup as follows:

  • Updated androidAppiumConfig.ts for Multiple Devices — Instead of a single capability object, define an array that supports multiple devices.
export const androidAppiumConfigs = [
{
platformName: "Android",
"appium:deviceName": process.env.ANDROID_DEVICE_NAME_1 || "emulator-5554",
"appium:udid": process.env.ANDROID_UDID_1 || "emulator-5554",
"appium:platformVersion": process.env.ANDROID_PLATFORM_VERSION_1 || "14",
"appium:automationName": "UiAutomator2",
"appium:app": "src/app/app.apk",
"appium:autoGrantPermissions": true,
"appium:chromedriverAutodownload": true,
"appium:ignoreHiddenApiPolicyError": true
},
{
platformName: "Android",
"appium:deviceName": process.env.ANDROID_DEVICE_NAME_2 || "emulator-5556",
"appium:udid": process.env.ANDROID_UDID_2 || "emulator-5556",
"appium:platformVersion": process.env.ANDROID_PLATFORM_VERSION_2 || "13",
"appium:automationName": "UiAutomator2",
"appium:app": "src/app/app.apk",
"appium:autoGrantPermissions": true,
"appium:chromedriverAutodownload": true,
"appium:ignoreHiddenApiPolicyError": true
}
];
  • Update wdio.config.ts to Support Parallel Execution — Now update your WDIO config to use multiple devices:
import { androidAppiumConfigs } from "./android.appium.config";

export const config: WebdriverIO.Config = {
capabilities: androidAppiumConfigs, // Pass multiple devices
maxInstances: androidAppiumConfigs.length, // Run as many instances as devices available

// Optional: Increase timeouts for multiple device execution
connectionRetryTimeout: 120000,
connectionRetryCount: 3,

// Optional: Parallel execution settings
services: ["appium"],
port: 4723, // Default Appium port
};
  • Start Appium Server for Multiple Devices
#Open multiple terminals and run:
appium -p 4723 --relaxed-security --allow-insecure adb_shell &
appium -p 4725 --relaxed-security --allow-insecure adb_shell &
  • Run WDIO Tests
AANDROID_DEVICE_NAME_1=emulator-5554 \
AANDROID_DEVICE_NAME_2=emulator-5556 \
npx wdio run src/config/wdio.conf.ts

6. Add Scripts to your Package.json

  • For ease of maintenance, you can add your commands to the package.json files and run directly
  "scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"run:wdio": "npx wdio run src/config/wdio.conf.ts",
"start:appium": "appium --relaxed-security",
"run:AndroidApp": "npx cross-env TEST_ENV=app PLATFORM=android npx wdio run src/config/wdio.conf.ts",
"run:AndroidChrome": "npx cross-env TEST_ENV=browser PLATFORM=android npx wdio run src/config/wdio.conf.ts",
"allure:report": "allure generate allure-results --clean -o allure-report && allure open allure-report"
},
  • Run as you wish
#Example : to run generate Allure report and open 
npm run allure:report

This is it for now, as I improve my project I’ll continue posting to help whoever is interested :)

Until then,

Thank you for reading and Happy Automating!! 🚀

--

--

Rochelle Abeywickrama
Rochelle Abeywickrama

Written by Rochelle Abeywickrama

Software Developer in Test at Yolo Group | MSc in IT (UK) | BSc in IT | ISTQB — CTFL | ISTQB — CTAL (ATM) | SAFe Agilist | Certified Scrum Master

Responses (1)