Skip to content

🤖 Enterprise-grade BDD mobile automation framework for iOS & Android using Appium, Cucumber, TestNG, and BrowserStack SDK. Thread-safe parallel execution with comprehensive reporting.

Notifications You must be signed in to change notification settings

bastheboss7/appium-ios-browserstack-bdd-java

Repository files navigation

📱 Mobile Automation BDD Framework

Scalable Appium Ecosystem for iOS & Android Real Devices

Appium BrowserStack Java Cucumber TestNG Build Architecture Score

📖 Project Overview

In modern mobile development, testing on a fragmented landscape of devices and OS versions is the primary bottleneck for Continuous Delivery. This project provides a production-ready, cloud-first automation solution designed to eliminate that bottleneck.

By combining the readability of Cucumber BDD with the massive scale of the BrowserStack Device Cloud, this framework allows teams to execute high-concurrency test suites on hundreds of real iOS and Android devices simultaneously.

💡 Core Value Proposition Zero Infrastructure Overhead: Moves away from flaky local emulators to stable, real-world cloud hardware.

Business-Readable Testing: Uses Gherkin syntax so Product Owners and QA can collaborate on test scenarios.

High-Speed Feedback: Built with a strict Thread-Local architecture, enabling massive parallel execution to reduce regression time from hours to minutes.

Shift-Left Ready: Native integration with GitHub Actions ensures every pull request is validated on real hardware before merge.

🏆 Framework Architecture Score

Current Rating: 9.0/10

✅ ThreadLocal driver isolation for parallel execution
✅ BrowserStack SDK integration with YAML-based configuration
✅ Page Object Model (POM) with platform-specific locators
✅ BDD implementation using Cucumber & Gherkin
✅ Automatic test retry mechanism with RetryAnalyzer
✅ Extent Reports with screenshot capture
✅ Platform-agnostic design (Android/iOS support)
✅ CI/CD ready with BrowserStack pipeline integration

🧪 Test Execution

  • Default TestNG suite: testngSuite.xml
  • Android (tag filter required):
mvn clean test \
    -DsuiteXmlFile=testngSuite.xml \
    -Dplatform=android \
    -Dbrowserstack.config=browserstack-android.yml \
    -Dcucumber.filter.tags="@androidOnly"
  • iOS (tag filter required):
mvn clean test \
    -DsuiteXmlFile=testngSuite.xml \
    -Dplatform=ios \
    -Dbrowserstack.config=browserstack-ios.yml \
    -Dcucumber.filter.tags="@iosOnly"
  • Never run without the platform-specific -Dcucumber.filter.tags; mixing Android/iOS steps in one run will fail. ├── main/java/ │ └── com/automation/framework/

⚙️ Configuration (BrowserStack-only)

  • Credentials via environment variables (BROWSERSTACK_USERNAME, BROWSERSTACK_ACCESS_KEY) or YAML (userName, accessKey).
  • Select platform via -Dplatform=android|ios (default android). BrowserStack SDK reads platforms list from YAML.
  • YAML lives at repo root; SDK injects capabilities directly. No config.properties files.
  • Waits: Configure via system property -DimplicitWait=10 or rely on BasePage defaults (10 seconds). Do NOT include frameworkOptions in YAML (unsupported by BrowserStack SDK).
  • browserstack.local is not used.

📁 Project Structure

├── main/java/
│   └── com/automation/framework/
│       ├── pages/
│       │   ├── BasePage.java               # Base class with waits/helpers
│       │   ├── HomeScreen.java             # Home screen POM
│       │   └── LoginScreen.java            # Login screen POM
│       ├── driver/
│       │   ├── DriverFactory.java          # Creates AppiumDriver instances
│       │   └── DriverManager.java          # ThreadLocal driver isolation
│       ├── config/
│       │   └── ConfigManager.java          # YAML config loader
│       └── utils/
│           ├── ElementActions.java         # Common element interactions
│           └── WaitUtils.java              # Explicit waits
├── test/java/
│   ├── runner/
│   │   └── TestNgRunner.java              # Cucumber + TestNG runner
│   ├── stepdefinitions/
│   │   └── WdioLoginSteps.java            # Step implementations
│   ├── listeners/
│   │   ├── ExtentReportListener.java      # Extent Reports integration
│   │   └── ExtentReportManager.java       # ThreadLocal report isolation
│   └── hooks/
│       └── Hooks.java                      # @Before/@After scenario hooks
├── test/resources/
│   └── features/
│       ├── BStackSample.feature           # BrowserStack test scenarios
│       └── wdioLogin.feature              # WDIO login test scenarios
├── pom.xml                                 # Maven dependencies & configuration
├── testngSuite.xml                         # TestNG suite (parallel execution)
├── browserstack-android.yml                # BrowserStack Android config
├── browserstack-ios.yml                    # BrowserStack iOS config
├── browserstack-android-ci.yml             # BrowserStack Android CI config
└── browserstack-ios-ci.yml                 # BrowserStack iOS CI config

Note: Parallel execution is configured in testngSuite.xml with parallel="methods" and thread-count="N". All test commands above will automatically use this parallel configuration.

🧵 Thread Safety & Parallel Execution

Overview

The framework uses a layered isolation model combining shared singletons for configuration and reporting with thread-local storage for driver and page object isolation. This enables safe parallel test execution without resource contention.

Key Components:

  • Shared Layer: ConfigManager (YAML config), ExtentReports (single report file)
  • Thread-Local Layer: AppiumDriver, PageObjectManager, ExtentTest instances
  • Cleanup: @After hook with try-finally ensures guaranteed resource cleanup

Component-Level Thread Safety

1️⃣ DriverManager - Thread-Local Driver Isolation

Each thread gets its own isolated AppiumDriver instance via ThreadLocal:

// DriverManager.java - Thread-safe driver management
public class DriverManager {
    // Each thread has its own driver instance - ZERO contention
    private static final ThreadLocal<AppiumDriver> driverThreadLocal = new ThreadLocal<>();
    
    public static AppiumDriver getDriver() {
        return driverThreadLocal.get();  // Returns THIS thread's driver only
    }
    
    public static void setDriver(AppiumDriver driver) {
        driverThreadLocal.set(driver);   // Sets driver for THIS thread only
    }
    
    public static void quitDriver() {
        AppiumDriver driver = driverThreadLocal.get();
        if (driver != null) {
            try {
                driver.quit();           // Quit THIS thread's driver
            } finally {
                driverThreadLocal.remove(); // CRITICAL: Prevent memory leak
            }
        }
    }
}

Why ThreadLocal?

  • No synchronization overhead (each thread has its own copy)
  • Zero contention between parallel tests
  • Memory is automatically cleaned up when remove() is called

2️⃣ DriverFactory - BrowserStack Cloud Connection

Each thread creates a driver that connects to BrowserStack cloud. The SDK handles device allocation:

// DriverFactory.java - BrowserStack cloud connection
public class DriverFactory {
    
    private static AppiumDriver createAndroidDriver() throws MalformedURLException {
        UiAutomator2Options options = new UiAutomator2Options();
        
        // Minimal options - BrowserStack SDK injects capabilities from YAML
        logger.info("Creating Android driver for BrowserStack cloud");
        
        // Each thread gets its own BrowserStack session
        return new AndroidDriver(URI.create(getBrowserStackHubUrl()).toURL(), options);
    }
    
    private static AppiumDriver createIOSDriver() throws MalformedURLException {
        XCUITestOptions options = new XCUITestOptions();
        
        // Minimal options - BrowserStack SDK injects capabilities from YAML
        logger.info("Creating iOS driver for BrowserStack cloud");
        
        // Each thread gets its own BrowserStack session
        return new IOSDriver(URI.create(getBrowserStackHubUrl()).toURL(), options);
    }
}

BrowserStack Cloud Benefits:

  • No port management needed (cloud handles isolation)
  • No local Appium server setup
  • Automatic device allocation from platform list in YAML
  • Real devices with real network conditions

3️⃣ PageObjectManager - Thread-Local Page Objects

Each thread gets its own set of page objects, preventing cross-contamination:

// PageObjectManager.java - Thread-isolated page objects
public class PageObjectManager {
    // Each thread gets its own PageObjectManager with its own page objects
    private static final ThreadLocal<PageObjectManager> instance = 
        ThreadLocal.withInitial(PageObjectManager::new);
    
    // Lazy-initialized page objects (created once per thread, reused)
    private HomeScreen homeScreen;
    private LoginScreen loginScreen;
    
    public static PageObjectManager getInstance() {
        return instance.get();  // Returns THIS thread's manager
    }
    
    public HomeScreen getHomeScreen() {
        if (homeScreen == null) {
            homeScreen = new HomeScreen();  // Lazy init for THIS thread
        }
        return homeScreen;
    }
    
    public LoginScreen getLoginScreen() {
        if (loginScreen == null) {
            loginScreen = new LoginScreen();  // Lazy init for THIS thread
        }
        return loginScreen;
    }
    
    public static void reset() {
        instance.remove();  // Clears THIS thread's manager + all page objects
    }
}

Step Definitions use composition (not inheritance):

// WdioLoginSteps.java - Clean composition pattern
public class WdioLoginSteps {
    // Method references avoid creating instances upfront
    private HomeScreen homeScreen() {
        return PageObjectManager.getInstance().getHomeScreen();
    }
    
    private LoginScreen loginScreen() {
        return PageObjectManager.getInstance().getLoginScreen();
    }

    @Given("I navigate to the Login screen")
    public void iNavigateToTheLoginScreen() {
        homeScreen().navigateToLogin();  // Gets THIS thread's HomeScreen
    }
}

4️⃣ ConfigManager - Thread-Safe Singleton

Configuration is loaded once and never modified (immutable after init):

// ConfigManager.java - Double-checked locking singleton
public class ConfigManager {
    private static final Properties properties = new Properties();
    private static boolean initialized = false;
    
    public static synchronized void init() {
        if (initialized) return;  // Fast-path: already initialized
        
        // Load properties (happens only ONCE across all threads)
        loadProperties("config.properties", properties);
        loadProperties("config-" + env + ".properties", properties);
        
        initialized = true;
    }
    
    public static String get(String key) {
        init();  // Ensure initialized (no-op if already done)
        // Safe to read from multiple threads - Properties is thread-safe for reads
        return System.getProperty(key, properties.getProperty(key));
    }
}

Why safe?

  • synchronized init() ensures one-time initialization
  • After init, properties are read-only (immutable)
  • java.util.Properties is thread-safe for concurrent reads

5️⃣ ExtentReportManager - Thread-Safe Reporting

Single report file with thread-local test instances:

// ExtentReportManager.java - Shared report, isolated tests
public class ExtentReportManager {
    private static ExtentReports extentReports;  // SHARED across threads
    private static final ThreadLocal<ExtentTest> extentTest = new ThreadLocal<>();  // ISOLATED
    
    public static synchronized void initReports() {
        if (extentReports == null) {
            // One-time initialization (synchronized)
            extentReports = new ExtentReports();
            extentReports.attachReporter(new ExtentSparkReporter(path));
        }
    }
    
    public static ExtentTest createTest(String testName) {
        initReports();
        // Thread-safe: createTest is synchronized internally by ExtentReports
        ExtentTest test = extentReports.createTest(testName);
        extentTest.set(test);  // Store in THIS thread's local
        return test;
    }
    
    public static void logPass(String message) {
        ExtentTest test = extentTest.get();  // Get THIS thread's test
        if (test != null) {
            test.pass(message);  // Log to THIS thread's test section
        }
    }
    
    public static synchronized void flushReports() {
        if (extentReports != null) {
            extentReports.flush();  // Write all results to file (once at end)
        }
    }
}

Architecture:

ExtentReports (Singleton)
├── Thread-1: ExtentTest "Login Scenario"
│   ├── Step: "Navigate to login" ✅
│   └── Step: "Enter credentials" ✅
├── Thread-2: ExtentTest "Forms Scenario"  
│   ├── Step: "Open forms" ✅
│   └── Step: "Submit form" ❌
└── Thread-N: ExtentTest "Swipe Scenario"
    └── ...

6️⃣ Hooks - Guaranteed Cleanup with try-finally

Critical for preventing resource leaks in parallel execution:

// Hooks.java - Guaranteed cleanup pattern
public class Hooks {
    
    @Before
    public void launchApplication(Scenario scenario) {
        ExtentReportManager.createTest(scenario.getName());
        DriverFactory.createDriver();  // Creates driver, stores in ThreadLocal
    }
    
    @After
    public void executeAfterScenario(Scenario scenario) {
        try {
            // Handle screenshots, logging, etc.
            if (scenario.isFailed() && DriverManager.hasDriver()) {
                byte[] screenshot = DriverManager.getDriver().getScreenshotAs(OutputType.BYTES);
                scenario.attach(screenshot, "image/png", "failure");
            }
        } catch (Exception e) {
            logger.warn("Screenshot failed: {}", e.getMessage());
        } finally {
            // ALWAYS executes - even if screenshot fails!
            PageObjectManager.reset();   // Clear page objects
            DriverManager.quitDriver();  // Quit driver + clear ThreadLocal
        }
    }
}

Why try-finally?

  • Ensures cleanup happens even if screenshot capture fails
  • Prevents driver/memory leaks that could crash parallel runs
  • Maintains isolation between test scenarios

Parallel Execution Memory Model

Each test thread has isolated copies of driver, page objects, and test reporting:

Component Isolation Level Storage Thread-Safe
AppiumDriver Per thread ThreadLocal ✅ ZERO contention
PageObjectManager Per thread ThreadLocal ✅ No shared state
ExtentTest Per thread ThreadLocal ✅ Isolated test nodes
ConfigManager Shared (read-only) Static singleton ✅ Synchronized init only
ExtentReports Shared (write-safe) Static singleton ✅ Internally synchronized

Running Parallel Tests

Tests execute in parallel via TestNG with isolated threads per scenario:

# Standard parallel execution (configured in testngSuite.xml)
mvn clean test \
    -DsuiteXmlFile=testngSuite.xml \
    -Dplatform=android \
    -Dbrowserstack.config=browserstack-android.yml \
    -Dcucumber.filter.tags="@androidOnly"

testngSuite.xml configuration:

<suite name="MobileAutomation-BDD-Suite" parallel="methods" thread-count="4">
    <listeners>
        <listener class-name="listeners.ExtentReportListener"/>
        <listener class-name="listeners.RetryTransformer"/>
    </listeners>
    <test name="Mobile-BDD-Tests">
        <classes>
            <class name="runner.TestNgRunner"/>
        </classes>
    </test>
</suite>

Parallelization Strategy:

  • parallel="methods": Each test method runs in its own thread
  • thread-count="4": Maximum 4 concurrent threads
  • Thread-local storage: Each thread maintains isolated driver, page objects, and test reporting

Thread Safety Checklist

Component Pattern Thread-Safe Notes
DriverManager ThreadLocal Each thread has isolated driver; BrowserStack cloud handles session isolation
PageObjectManager ThreadLocal Each thread has isolated page objects; cleaned up via @After hook
ExtentReportManager ThreadLocal + Synchronized Shared report file with isolated test nodes per thread
ConfigManager Synchronized init One-time YAML load; immutable after initialization
DriverFactory BrowserStack SDK SDK manages device allocation and capabilities injection
BasePage Stateless Accesses thread's driver via DriverManager.getDriver()
Hooks try-finally Guaranteed cleanup prevents memory leaks in parallel execution

🎯 Key Design Patterns

Pattern Implementation Benefit
Page Object Model BasePageHomeScreen/LoginScreen → Step Definitions Separation of concerns, maintainable
Factory Pattern DriverFactory with atomic port allocation Platform-agnostic, thread-safe driver creation
Singleton Pattern ConfigManager with synchronized init Centralized multi-env configuration
Thread-Local DriverManager, PageObjectManager, ExtentTest Complete thread isolation for parallel execution
Composition over Inheritance Step definitions use PageObjectManager.getInstance() Flexible, avoids diamond problem
SDK Integration BrowserStack Java SDK manages capabilities, devices Eliminates manual configuration overhead
Lazy Initialization PageObjectManager.getHomeScreen() Memory efficient, on-demand creation
Template Method BasePage defines common actions DRY, consistent element interactions
Strategy Pattern Element-based assertions with fallbacks Reliable mobile element detection

🚀 Quick Start

Prerequisites

  • Java 21+
  • Maven 3.8+
  • BrowserStack account with active credentials

Installation

# Clone the repository
git clone <repository-url>
cd MobileAutomation_BDD_Framework

# Install dependencies
mvn clean install -DskipTests

Running Tests

All tests run on BrowserStack cloud infrastructure using the SDK agent. See the BrowserStack (SDK-Only) section for detailed instructions.


🔐 Security & Credentials

Local Development

# Set environment variables before running tests
export BROWSERSTACK_USERNAME=your-username
export BROWSERSTACK_ACCESS_KEY=your-access-key

# Run tests (DriverFactory falls back to env vars if YAML not found)
mvn clean test \
    -DsuiteXmlFile=testngSuite.xml \
    -Dplatform=android \
    -Dbrowserstack.config=browserstack-android.yml \
    -Dcucumber.filter.tags="@androidOnly"

CI/CD (GitHub Actions)

  • Credentials stored as repository secrets: BROWSERSTACK_USERNAME, BROWSERSTACK_ACCESS_KEY
  • GitHub Actions injects via environment variables
  • YAML files browserstack-*.yml are in .gitignore to prevent accidental commits

Why Not Hardcode Credentials?

  • ❌ Exposes secrets in version control
  • ❌ Requires credential rotation for every developer leaving team
  • ❌ Violates compliance (SOC 2, ISO 27001)
  • ✅ Use environment variables instead (one secret per environment)

📋 Configuration Files Explained

Default Behavior (Not Recommended for Production)

  • pom.xml sets default: browserstack-android.yml
  • Running: mvn clean test -Dplatform=android → loads browserstack-android.yml

Explicit Config (Recommended)

  • Override with: mvn clean test -Dplatform=ios -Dbrowserstack.config=browserstack-ios-ci.yml
  • Runs: loads browserstack-ios-ci.yml instead of default

File Locations & Usage

File Purpose Location Credentials
browserstack-android.yml Android development/local tests Repo root ${BROWSERSTACK_USERNAME}, ${BROWSERSTACK_ACCESS_KEY}
browserstack-ios.yml iOS development/local tests Repo root ${BROWSERSTACK_USERNAME}, ${BROWSERSTACK_ACCESS_KEY}
browserstack-android-ci.yml Android CI/CD pipeline tests Repo root ${BROWSERSTACK_USERNAME}, ${BROWSERSTACK_ACCESS_KEY} (injected by GitHub Actions)
browserstack-ios-ci.yml iOS CI/CD pipeline tests Repo root ${BROWSERSTACK_USERNAME}, ${BROWSERSTACK_ACCESS_KEY} (injected by GitHub Actions)

How ConfigManager Loads Files:

  1. Looks for file specified in -Dbrowserstack.config parameter
  2. Falls back to pom.xml default if not specified
  3. Reads from repo root, src/test/resources/, or target/test-classes/ (in that order)
  4. All environment variable placeholders (${BROWSERSTACK_USERNAME}) are resolved at runtime

BrowserStack (SDK-Only)

Overview

  • SDK-Driven: BrowserStack Java SDK agent manages all capabilities, app upload, and session lifecycle.
  • No hubUrl in YAML: SDK sets hub endpoint internally via agent. Remove hubUrl from config.
  • Credentials: Load via environment variables (BROWSERSTACK_USERNAME, BROWSERSTACK_ACCESS_KEY). Never hardcode for CI/CD.
  • App Reference: Use custom_id (string identifier) or bs://<id> (uploaded app ID) to avoid stale references.

Android

  • Prerequisites: BrowserStack account; Java 21+; Maven 3.8+.
  • Config: browserstack-android.yml with:
    • app: custom_id:my-android-app (recommended) OR app: bs://uploaded-app-id
    • NO hubUrl (SDK handles endpoint)
    • platforms with device names and OS versions
  • Credentials: Provide via environment:
export BROWSERSTACK_USERNAME=<your-username>
export BROWSERSTACK_ACCESS_KEY=<your-access-key>
  • Run:
mvn clean test \
    -DsuiteXmlFile=testngSuite.xml \
    -Dplatform=android \
    -Dbrowserstack.config=browserstack-android.yml \
    -Dcucumber.filter.tags="@androidOnly"
  • Verify: BrowserStack dashboard shows sessions; logs confirm app ID and device allocation.

iOS

  • Prerequisites: BrowserStack account; Java 21+; Maven 3.8+.
  • Config: browserstack-ios.yml with:
    • app: custom_id:my-ios-app (recommended) OR app: bs://uploaded-app-id
    • NO hubUrl (SDK handles endpoint)
    • platforms with device names and OS versions
  • Credentials: Provide via environment (same as Android above).
  • Run:
mvn clean test \
    -DsuiteXmlFile=testngSuite.xml \
    -Dplatform=ios \
    -Dbrowserstack.config=browserstack-ios.yml \
    -Dcucumber.filter.tags="@iosOnly"
  • Verify: BrowserStack dashboard shows sessions; logs confirm app ID and device allocation.

Platform Tag Filtering (Critical)

All platform-specific commands MUST include the -Dcucumber.filter.tags parameter to isolate tests by platform. This prevents cross-platform test contamination and ensures correct locator evaluation.

Why Tag Filtering is Essential

  • Locator Compatibility: Android and iOS interpret XPath, CSS, and ID locators differently. Running Android-only tests on iOS (or vice versa) causes ClassCastException and element interaction failures.
  • Driver Response Marshalling: The BrowserStack Java SDK formats element responses differently per platform. Platform-aware tests account for these differences.
  • Step Isolation: Steps marked with @androidOnly use Android-specific navigation; steps marked with @iosOnly use iOS-specific gestures.

Tag Usage

  • @androidOnly: Tests/steps that execute ONLY on Android
  • @iosOnly: Tests/steps that execute ONLY on iOS
  • @BStackSample: Shared scenarios (not platform-specific; optional)

Commands with Proper Filtering

# Android: Run only @androidOnly scenarios
mvn clean test \
    -DsuiteXmlFile=testngSuite.xml \
    -Dplatform=android \
    -Dbrowserstack.config=browserstack-android.yml \
    -Dcucumber.filter.tags="@androidOnly"

# iOS: Run only @iosOnly scenarios
mvn clean test \
    -DsuiteXmlFile=testngSuite.xml \
    -Dplatform=ios \
    -Dbrowserstack.config=browserstack-ios.yml \
    -Dcucumber.filter.tags="@iosOnly"

# Multi-tag (AND logic): Run scenarios tagged with both @androidOnly AND @smoke
mvn clean test \
    -DsuiteXmlFile=testngSuite.xml \
    -Dplatform=android \
    -Dbrowserstack.config=browserstack-android.yml \
    -Dcucumber.filter.tags="@androidOnly and @smoke"

# Exclusive execution (NOT logic): Run all scenarios EXCEPT @androidOnly (useful for iOS)
mvn clean test \
    -DsuiteXmlFile=testngSuite.xml \
    -Dplatform=ios \
    -Dbrowserstack.config=browserstack-ios.yml \
    -Dcucumber.filter.tags="not @androidOnly"

Common Mistakes

  • Missing tag filter: mvn clean test -Dplatform=ios -Dbrowserstack.config=browserstack-ios.yml → Runs ALL scenarios including @androidOnly, causing failures on iOS.
  • Wrong tag: mvn clean test -Dplatform=ios -Dbrowserstack.config=browserstack-ios.yml -Dcucumber.filter.tags="@androidOnly" → Runs Android tests on iOS; element interactions fail.
  • Typo in tag: mvn clean test -Dplatform=android -Dbrowserstack.config=browserstack-android.yml -Dcucumber.filter.tags="@androidonly" (lowercase) → Tag doesn't match (case-sensitive); no tests run.

Example Feature File Structure

@androidOnly
Scenario: Login with valid credentials (Android-specific)
  Given User opens the wdiodemoapp on Android
  When User enters username and taps login
  Then Login success is displayed

@iosOnly
Scenario: Verify Alert Functionality (iOS-specific)
  Given User opens the BStackSampleApp on iOS
  When User taps the alert trigger button
  Then Alert is displayed
  And User accepts the alert

@BStackSample
Scenario: Shared flow (both platforms)
  Given User opens the app
  When User performs a generic action
  Then Generic success is displayed

Always validate your feature file tags match your -Dcucumber.filter.tags parameter before running tests.


🔄 CI/CD with GitHub Actions

The framework includes comprehensive GitHub Actions workflows for automated testing in CI/CD pipelines.

Available Workflows

1. BrowserStack SDK Tests (browserstack-sdk.yml)

Automated BrowserStack testing using SDK agent for both Android and iOS.

Triggers:

  • Push to main, develop, appiumMobile branches
  • Pull requests to main, develop
  • Manual dispatch with platform selection

Jobs:

  • ✅ Build & Validate
  • 🤖 Android SDK Tests (parallel across 3 devices)
  • 🍎 iOS SDK Tests (parallel across 3 devices)
  • 📊 Test Summary & Report Generation

Manual Trigger:

# Via GitHub UI: Actions tab → BrowserStack SDK Tests → Run workflow
# Select platform: android | ios | both
# Optional: Custom Cucumber tags (defaults: @androidOnly, @iosOnly)

Required Secrets:

BROWSERSTACK_USERNAME=<your-browserstack-username>
BROWSERSTACK_ACCESS_KEY=<your-browserstack-access-key>

Setting Up GitHub Actions

Step 1: Add BrowserStack Secrets

  1. Go to SettingsSecrets and variablesActions
  2. Add repository secrets:
    • BROWSERSTACK_USERNAME: Your BrowserStack username
    • BROWSERSTACK_ACCESS_KEY: Your BrowserStack access key

Step 2: Enable Workflows

Workflows are automatically enabled after pushing to the repository. Verify:

git push origin appiumMobile

Step 3: Monitor Execution

  • Navigate to Actions tab in GitHub
  • View real-time logs for each job
  • Download artifacts (test reports, logs) after completion

Workflow Configuration Examples

Example 1: Android SDK Tests Only

# Triggered automatically on push/PR
# Or manually with platform selection
jobs:
  test-android-sdk:
    runs-on: ubuntu-latest
    steps:
      - name: Run Android SDK Tests
        env:
          BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
          BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
        run: |
          mvn clean test \
            -Pbrowserstack \
            -Dplatform=android \
            -Denv=browserstack \
            -Dcucumber.filter.tags="@androidOnly"

Example 2: Custom Tags for Smoke Tests

# Manual workflow dispatch with custom tags
workflow_dispatch:
  inputs:
    tags:
      description: 'Cucumber tags filter'
      default: '@Smoke'

# Usage in job:
-Dcucumber.filter.tags="${{ github.event.inputs.tags }}"

Example 3: Parallel Platform Testing

# Both platforms run in parallel (independent jobs)
jobs:
  test-android-sdk:
    runs-on: ubuntu-latest
    # Android tests run independently
  
  test-ios-sdk:
    runs-on: ubuntu-latest
    # iOS tests run independently (no dependency on Android)

Test Reports & Artifacts

All workflows upload test reports as artifacts:

Artifact Types:

  • 📊 Extent Reports: HTML reports with screenshots and step details
  • 📋 Surefire Reports: XML test results for GitHub UI integration
  • 📝 Cucumber Reports: JSON reports for trend analysis
  • 🔍 Logs: Debug logs (uploaded only on failure)

Accessing Artifacts:

  1. Go to Actions → Select workflow run
  2. Scroll to Artifacts section at the bottom
  3. Download zip files (e.g., android-sdk-reports.zip)
  4. Extract and open extent-reports/ExtentReport.html

Artifact Retention:

  • Test reports: 30 days
  • Debug logs: 7 days

Best Practices for CI/CD

  1. Branch Protection Rules:

    # Require BrowserStack SDK tests to pass before merging
    Settings → Branches → main → Require status checks:
    - Android SDK Test Results ✅
    - iOS SDK Test Results ✅
  2. Scheduled Runs:

    # Add to workflow for nightly regression
    on:
      schedule:
        - cron: '0 2 * * *'  # 2 AM UTC daily
  3. Slack Notifications:

    # Add step to notify on failure
    - name: Notify Slack
      if: failure()
      uses: slackapi/slack-github-action@v1
      with:
        webhook-url: ${{ secrets.SLACK_WEBHOOK }}
  4. Dependency Caching:

    # Already configured in workflows
    - uses: actions/cache@v4
      with:
        path: ~/.m2/repository
        key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
  5. Matrix Strategy for Multiple Environments:

    strategy:
      matrix:
        environment: [staging, production]
        platform: [android, ios]
    # Runs 4 jobs: staging-android, staging-ios, prod-android, prod-ios

Troubleshooting CI/CD Issues

Issue Solution
BrowserStack credentials error Verify secrets are added: BROWSERSTACK_USERNAME, BROWSERSTACK_ACCESS_KEY
No tests executed Check Cucumber tag filter matches feature file tags (case-sensitive)
Maven dependency timeout Enable dependency caching; increase MAVEN_OPTS memory
iOS Simulator boot failure Use macos-13 runner (has Xcode 14); macos-latest may vary
Parallel execution conflicts Ensure proper port management in DriverFactory (already configured)
Artifact upload size limit Compress large reports; reduce screenshot size in Extent config

Workflow Status Badges

Add status badges to README:

[![BrowserStack SDK Tests](https://github.com/<owner>/<repo>/actions/workflows/browserstack-sdk.yml/badge.svg)](https://github.com/<owner>/<repo>/actions/workflows/browserstack-sdk.yml)

About

🤖 Enterprise-grade BDD mobile automation framework for iOS & Android using Appium, Cucumber, TestNG, and BrowserStack SDK. Thread-safe parallel execution with comprehensive reporting.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published