This repository contains a starter boilerplate for working with WebdriverIO, a test automation framework for web browsers and mobile applications.
Make sure you have Node.js installed. Download it from nodejs.org.
Recommended setup resources:
- Blog guide: Appium v2 Android Setup Guide
- YouTube video: Appium v2 - WebdriverIO (some CLI options may have changed since recording)
- Clone this repository:
git clone https://github.com/charlyautomatiza/boilerplate-webdriverio.git- Change into the project directory:
cd boilerplate-webdriverio- Install dependencies:
npm installDownload the latest release of the WebdriverIO Native Demo (Guinea Pig) App for Android (and iOS if needed).
Create a folder named app at the project root and place the APK there to avoid path issues.
After configuring your environment variables (ANDROID_HOME / Java etc.), run the tests:
npm run wdioThis command runs the E2E tests and produces Allure + JUnit reports.
npm run open-reportThe included GitHub Actions workflow automatically runs the E2E tests on an Android emulator. It:
- Downloads the demo APK into
app/ - Grants required emulator permissions
- Boots an Android emulator (API 34 / Android 14) via
reactivecircus/android-emulator-runner - Installs dependencies (Node.js, Appium, WebdriverIO, etc.)
- Executes the E2E tests
- Publishes Allure and JUnit results as artifacts
Triggers: on every push or pull request to main.
See .github/workflows/android-emulator.yml for full details.
This project showcases advanced testing patterns (order reflects appearance in the spec file):
Validates the core happy path (sanity check for environment, selectors, and app readiness).
it('should login with valid credentials', async () => {
await LoginPage.loginBtn.click();
await LoginPage.login('[email protected]', 'SuperSecretPassword!');
await expect(AlertPage.messageAlert).toHaveText(expect.stringContaining('You are logged in!'));
});Clarifies test intent by separating setup, action, and verification.
it('should login successfully following the AAA pattern', async () => {
// Arrange
await LoginPage.loginBtn.click();
await expect(LoginPage.inputUsername).toBeDisplayed();
await expect(LoginPage.inputPassword).toBeDisplayed();
await expect(LoginPage.btnSubmit).toBeDisplayed();
// Act
await LoginPage.login('[email protected]', 'SuperSecretPassword!');
// Assert
await expect(AlertPage.messageAlert).toHaveText(
expect.stringContaining('You are logged in!'),
{ message: 'Confirmation message is not the expected one' }
);
});Each JSON row generates its own test case using a for...of loop for granular reporting.
// Load JSON data once
const jsonData: LoginData[] = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
for (const row of jsonData) {
it(`DDT JSON - User: "${row.username}" → message: "${row.expectedMessage}"`, async () => {
// Arrange
await LoginPage.loginBtn.click();
// Act
await LoginPage.login(row.username, row.password);
// Assert (implicit wait via expect)
await expect(AlertPage.messageAlert).toHaveText(
expect.stringContaining(row.expectedMessage),
{ message: `Expected message: "${row.expectedMessage}" for user: ${row.username}` }
);
});
}Data file: test/data/loginData.json
[
{ "username": "[email protected]", "password": "SuperSecretPassword!", "expectedMessage": "You are logged in!" },
{ "username": "[email protected]", "password": "TestPass123!", "expectedMessage": "You are logged in!" },
{ "username": "[email protected]", "password": "Admin2024!", "expectedMessage": "You are logged in!" }
]CSV parsing transforms raw text into structured objects. Each row becomes an independent test for clearer failure isolation.
// Load and parse CSV at runtime
const csvContent = fs.readFileSync(filePath, 'utf-8');
const lines = csvContent.trim().split('\n');
const userData: UserData[] = lines.slice(1).map(line => {
const values = line.split(',').map(v => v.replace(/"/g, '').trim());
return {
username: values[0],
password: values[1],
expectedAction: values[2]
};
});
for (const row of userData) {
it(`DDT CSV - User: "${row.username}" → expected action: ${row.expectedAction}` , async () => {
// Arrange
await LoginPage.loginBtn.click();
// Act
await LoginPage.login(row.username, row.password);
// Assert
await expect(AlertPage.messageAlert).toHaveText(
expect.stringContaining(row.expectedAction)
);
});
}Data file: test/data/buttonsData.csv
username,password,expectedAction
[email protected],Pass123!,login_success
[email protected],Pass456!,login_successAlready documented as example 2 to reflect execution order.
it('should login successfully following the AAA pattern', async () => {
// Arrange
await LoginPage.loginBtn.click();
await expect(LoginPage.inputUsername).toBeDisplayed();
await expect(LoginPage.inputPassword).toBeDisplayed();
await expect(LoginPage.btnSubmit).toBeDisplayed();
// Act
await LoginPage.login('[email protected]', 'SuperSecretPassword!');
// Assert
await expect(AlertPage.messageAlert).toHaveText(
expect.stringContaining('You are logged in!'),
{ message: 'Confirmation message is not the expected one' }
);
});Avoid forEach for async tests—for...of preserves proper async flow and allows dynamic test titles.
expect(...).toHaveText() inherently waits for the condition, reducing flakiness versus fixed sleeps.
Custom messages help pinpoint data row failures quickly.
All app interactions go through LoginPage and AlertPage, centralizing selectors and actions.
// test/pageobjects/login.page.ts
class LoginPage {
public get loginBtn () {
return $('~Login');
}
public get inputUsername () {
return $('~input-email');
}
public get inputPassword () {
return $('~input-password');
}
public async login (username: string, password: string) {
await this.inputUsername.setValue(username);
await this.inputPassword.setValue(password);
await this.btnSubmit.click();
}
}test/
├── data/
│ ├── loginData.json # Data file for JSON-driven tests
│ └── buttonsData.csv # Data file for CSV-driven tests
├── pageobjects/
│ ├── login.page.ts # Login Page Object
│ └── alert.page.ts # Alert Page Object
├── types/
│ └── data.ts # Shared TypeScript interfaces (LoginData, UserData)
└── specs/
└── test.e2e.ts # E2E tests (basic, AAA, JSON DDT, CSV DDT)
Current suite produces (in execution order):
- 1 basic login test
- 1 AAA pattern test
- 3 JSON DDT test cases
- 2 CSV DDT test cases
Allure & JUnit reports show distinct titles per dataset row for clarity.
- Basic Login: Verifies essential flow and environment stability before deeper scenarios.
- AAA Pattern: Reinforces explicit structure for clarity and future maintenance.
- JSON DDT: Scales rapidly—add rows to expand coverage without modifying test logic.
- CSV DDT: Demonstrates ingestion of spreadsheet-style data for flexible scenario expansion.
- Extract interfaces (
LoginData,UserData) outside thedescribefor reuse. - Add negative login scenarios (invalid password / empty fields) to demonstrate error handling.
- Add screenshot / log attachment steps on failure integrated into Allure.
- Parameterize device capabilities for multi-platform matrix (Android/iOS).
- Introduce test tagging to selectively run subsets (e.g., smoke vs regression).
| Anti-Pattern | Why It Hurts | Better Approach |
|---|---|---|
Hard-coded sleeps (e.g. browser.pause(3000)) |
Brittle & slow; ignores real UI readiness | Implicit waits via WebdriverIO expectations (toBeDisplayed, toHaveText) |
Using forEach for async test generation |
Does not await properly; can create false positives | Use for...of to generate discrete it blocks |
| Mixing test logic & selectors inline | Duplicates selectors; hard to refactor | Page Object Model centralizes selectors/actions |
| Long monolithic test cases | Hard to debug; single failure hides others | Split by scenario/data; keep tests focused |
| Global mutable state between tests | Causes leakage & flaky order-dependent results | Reset app/session in hooks (afterTest) |
| Overuse of generic XPath selectors | Fragile when UI shifts; performance overhead | Prefer accessibility IDs / resource IDs |
| Ignoring platform differences | Unexpected failures on iOS vs Android | Abstract platform-specific selectors/methods |
| Silent assertions (no message) | Harder triage in large suites | Provide assertion messages with contextual data |
| No reporting attachments | Limited diagnostics for CI failures | Capture screenshots/logcat on failure |
| Testing multiple flows in one test | One failure invalidates many checks | Keep single, clear purpose per test |
Ensuring a clean app state after each test prevents leakage (e.g., leftover logged-in session) that can hide defects. The current afterTest hook invokes browser.relaunchActiveApp(), offering a lightweight refresh versus a full session recreation, reducing execution time while still clearing in-app UI state.
- Deterministic tests (no arbitrary waits)
- Readable structure (AAA, clear titles)
- Data externalization for scalability
- Fast failure diagnosis (granular test cases + messages)
- Maintainability (POM encapsulation)
Using centralized TypeScript interfaces (LoginData, UserData in test/types/data.ts) provides several concrete benefits:
- Single Source of Truth: Changing a field (e.g. renaming
expectedMessage) updates all specs and utilities at compile time—no silent drift. - Early Feedback: Type errors surface during development instead of causing runtime
undefinedor assertion mismatches in CI. - Safer Refactors: IDE auto-complete and rename tools work reliably across the suite, reducing regressions in large data-driven expansions.
- Data Contract Clarity: New contributors immediately see required fields and their intent, lowering onboarding friction.
- Prevents “Magic” Fields: Explicit interfaces discourage ad‑hoc additions to JSON/CSV that tests forget to assert.
- Enables Reuse: Utilities (parsers, generators, factories) can accept typed objects, improving composability and test scaffolding.
- Facilitates Lint & Static Analysis: Linters and quality tools (e.g. SonarQube) reason better about well-defined shapes versus dynamic objects.
Anti‑Pattern Avoided: Defining interfaces inline inside each describe causes duplication and accidental divergence; centralizing them avoids this drift.
Feel free to open issues or submit pull requests for enhancements and fixes.
Need help? Open an issue. More info: WebdriverIO Docs.
Additional content & updates: Website • Community Discord: Join here.