How to Automate Offline Mode Testing (Step-by-Step)

How to Automate Offline Mode Testing (Step-by-Step)

January 02, 2026 · 16 min read · How-To Guides

How to Automate Offline Mode Testing (Step-by-Step)

Offline mode testing validates that an application behaves correctly when network connectivity is absent or intermittent. Automating these checks reduces regression risk, catches edge‑cases that appear only after a device loses signal, and provides fast feedback in CI pipelines. This guide walks you through the entire process: from deciding when automation is worthwhile, picking a framework, building reliable locators and waits, to integrating tests into CI and reporting results. Real code snippets for Android (Appium), web (Playwright), and an autonomous‑exploration approach with SUSA are included, plus a test‑matrix table and a tool‑comparison table you can bookmark for reference.

1. Understanding Offline Mode and Why Automate

1.1 What Constitutes Offline Mode

Offline mode covers several network states:

1.2 Business Impact of Offline Failures

When an app crashes, shows stale data, or blocks core flows offline, users abandon the product, support tickets rise, and brand trust erodes. Automated offline tests surface these defects early, before they reach production, and they run repeatedly without manual effort, making them a high‑ROI investment for any team that ships mobile or web experiences.

1.3 When Automation Is the Right Choice

Automation pays off when:

If any of these conditions are false, a lightweight manual smoke check may suffice, but for most products the overhead of automation is outweighed by the reduction in regressions.

2. When Automation Pays Off: Criteria and ROI

2.1 Decision Matrix

CriterionManual TestingAutomated TestingComment
Test frequency (per release)Low (<1)High (≥5)Automation shines when you run the same steps many times.
Setup complexity (device/network)High (manual toggles)Low (scripted)Scripts eliminate human error in toggling airplane mode or applying throttling.
Flakiness toleranceModerate (human can retry)Low (needs stable locators)Requires investment in stable selectors and waits.
Skill investmentLow (tester)Moderate (dev/QA)Requires familiarity with chosen framework.
Defect detection latencyHours‑daysMinutesFast feedback reduces mean quicker fixes.
Long‑term maintenanceLow (no code)Moderate (test updates)Higher (test updatesScore each criterion3‑> means automation is likely the investment.

2.2 Quick ROI Calculation

Assume:

Manual effort per sprint = 8 min × 12 = 96 min ≈ 1.6 h.

Automated effort per sprint = 2 min × 12 = 24 min + 4 h (amortized over 4 sprints) ≈ 3 h.

After the first sprint, automation saves ~1.4 h per sprint and continues to accrue savings as the test suite grows. In addition, each caught defect saves hours of debugging and potential rollback costs.

3. Choosing the Right Test Framework for Offline Testing

3.1 Factors to Evaluate

3.2 Tool‑Comparison Table

FrameworkPlatformNetwork ControlLanguageCI IntegrationFlakiness HelpersTypical Setup Time
AppiumAndroid/iOS/hybridADB shell commands, network speed via emulator, tc on real devicesJava, JS, Python, C#, RubyJenkins, GitHub Actions, GitLab CIExplicit waits, ExpectedConditions, screenshot on failureMedium (requires server)
EspressoAndroid onlyadb shell svc wifi disable, adb shell svc data disableJava/KotlinGradle, Android Studio, CI via Firebase Test LabIdlingResource, automatic synchronizationLow (in‑app)
XCTestiOS onlyNetwork Link Conditioner, osascript to toggle airplane modeSwift/Objective‑CXcode Server, Bitrise, GitHub ActionsExpectation APIs, synchronous expectationsLow (in‑app)
PlaywrightWeb (Chromium/Firefox/WebKit)context.setOffline(true), context.setNetworkConditionsJS/TS, Python, Java, .NETGitHub Actions, Azure Pipelines, CircleCIAuto‑waits, trace viewer, retry on failureLow (no server)
CypressWeb (Chromium/Firefox)cy.intercept to simulate failures, cy.route to throttleJS/TSGitHub Actions, CircleCIBuilt‑in retry, time‑travel debuggingLow (no server)
SUSA (autonomous)Android/WebExplores app autonomously, injects network loss via device‑agentNo code (config‑based)CLI agent, GitHub Actions via susatest-agentSelf‑healing locators, cross‑session learningVery low (upload APK/URL)

How to pick

4. Setting Up the Test Environment: Emulators, Devices, Network Simulation

4.1 Emulators vs. Physical Devices

4.2 Network‑Simulation Techniques

TechniqueToolCommand ExampleWhat It Simulates
Airplane modeADB (adb shell svc wifi disable && adb shell svc data disable)adb shell svc wifi disable && adb shell svc data disableTotal loss of connectivity
Wi‑Fi only offADBadb shell svc wifi disable svc wifi disableSimul Wi‑Fi only offADBadb shell svc data enable && adb shell svc wifi disableCellular‑only scenario
Cellular only offADBadb shell svc data disable && adb shell svc wifi enableWi‑Fi‑only scenario
Latency & bandwidthtc (Linux traffic control) on rooted device or emulatoradb shell tc qdisc add dev wlan0 root netem delay 200ms limit 1000200 ms one‑way latency
Network Link Conditioner (iOS)Xcode device settingsxcrun simctl spawn booted uiaccessibility (or use Simulator menu)Pre‑defined profiles (Good 3G, LTE, Lossy)
Playwright throttlingcontext.setNetworkConditions`await context.setNetworkConditions({ offline: false, latency: 150, downloadThroughput: 500 * 1024, uploadThroughput: 500 * 1024 });`Custom throttling
SUSA autonomous lossBuilt‑in agentsusatest-agent run --apk myapp.apk --network-loss 30Random 30 second loss intervals during exploration

4.3 Preparing the Device Farm

  1. Install required agents – For Appium, ensure the Appium server version matches the device OS. For Espresso/XCTest, no extra server is needed.
  2. Grant necessary permissionsandroid.permission.ACCESS_NETWORK_STATE, android.permission.CHANGE_NETWORK_STATE, and for iOS enable “Network Extension” entitlement if you plan to toggle radios via private APIs (only for testing builds).
  3. Enable USB debugging (Android) or allow remote automation (iOS via WebDriverAgent).
  4. Verify network‑control commands work on a sample device before scaling to the farm.

5. Designing a Stable Offline Mode Test Suite

5.1 Locator Strategy for Offline Scenarios

5.2 Handling Waits and Flakiness

5.3 Data Setup and Teardown

5.4 Test Organization

Group tests by feature (login, checkout, profile) and then by network condition (online, offline, intermittent). This matrix makes it easy to spot gaps and to run a subset (e.g., only offline tests) when debugging a specific issue.


tests/
├── login/
│   ├── online.test.js
│   ├── offline.test.js
│   └── intermittent.test.js
├── checkout/
│   ├── online.test.js
│   └── offline.test.js
└── utils/
    ├── network.js
    └── fixtures.js

6. Writing Offline Mode Tests: Step‑by‑Step Examples

6.1 Android Offline Test with Appium (Java)


package com.example.tests;

import io.appium.java_client.AppiumDriver;
import io.appium.java_client.MobileElement;
import io.appium.java_client.android.AndroidDriver;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;

import java.net.URL;
import java.time.Duration;

public class OfflineLoginTest {

    private AppiumDriver<MobileElement> driver;
    private WebDriverWait wait;

    @Before
    public void setUp() throws Exception {
        // Desired capabilities for a Pixel 4 API 33 emulator
        var caps = new io.appium.java_client.remote.MobileCapabilityType[]{
                io.appium.java_client.remote.MobileCapabilityType.PLATFORM_NAME, "Android",
                io.appium.java_client.remote.MobileCapabilityType.DEVICE_NAME, "Pixel_4_API_33",
                io.appium.java_client.remote.MobileCapabilityType.APP, "/path/to/app-debug.apk",
                io.appium.java_client.remote.MobileCapabilityType.AUTOMATION_NAME, "UiAutomator2"
        };
        var options = new io.appium.java_client.android.options.UiAutomator2Options();
        options.merge(caps);
        driver = new AndroidDriver<>(new URL("http://localhost:4723/wd/hub"), options);
        wait = new WebDriverWait(driver, Duration.ofSeconds(15));
    }

    @After
    public void tearDown() {
        if (driver != null) {
            driver.quit();
        }
    }

    /** Helper to toggle airplane mode via ADB */
    private void setAirplaneMode(boolean enable) throws Exception {
        Runtime.getRuntime().exec(
                new String[]{"adb", "-s", emulatorId(), "shell", "svc", "wifi", enable ? "disable" : "enable"});
        Runtime.getRuntime().exec(
                new String[]{"adb", "-s", emulatorId(), "shell", "svc", "data", enable ? "disable" : "enable"});
    }

    private String emulatorId() {
        // In CI you can pass the device serial as env var
        return System.getenv().getOrDefault("EMULATOR_SERIAL", "emulator-5554");
    }

    @Test
    public void loginShouldShowOfflineBannerWhenNoNetwork() throws Exception {
        // Start with online state to perform login
        setAirplaneMode(false);
        driver.launchApp();

        // Perform login (online)
        MobileElement email = wait.until(ExpectedConditions.elementToBeClickable(By.id("email_input")));
        email.sendKeys("test@example.com");
        MobileElement pwd = driver.findElement(By.id("password_input"));
        pwd.sendKeys("SecurePass!123");
        MobileElement loginBtn = driver.findElement(By.id("login_button"));
        loginBtn.click();

        // Wait for home screen to appear (indicates successful online login)
        wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("home_toolbar")));

        // Simulate loss of connectivity
        setAirplaneMode(true);

        // Trigger a network‑dependent action (e.g., pull‑to‑refresh)
        MobileElement refresh = driver.findElement(By.id("refresh_button"));
        refresh.click();

        // Verify offline banner appears
        MobileElement banner = wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("offline_banner")));
        assert banner.isDisplayed();
        assert banner.getText().equals("No internet connection");

        // Restore connectivity and ensure app recovers
        setAirplaneMode(false);
        MobileElement retryBtn = driver.findElement(By.id("retry_button"));
        retryBtn.click();
        // Expect home screen to reappear after sync
        wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("home_toolbar")));
    }
}

Key points

6.2 Web Offline Test with Playwright (TypeScript)


import { test, expect } from '@playwright/test';

test.describe('Offline checkout flow', () => {
  test.use({ viewport: { width: 1280, height: 800 } });

  test.beforeEach(async ({ page }) => {
    // Start online, navigate to product page
    await page.goto('https://shop.example.com/product/42');
    await expect(page.locator('#add-to-cart')).toBeEnabled();
  });

  test('should show offline cart and allow purchase later', async ({ page, context }) => {
    // Add item while online
    await page.click('#add-to-cart');
    await expect(page.locator('#cart-badge')).toHaveText('1');

    // Simulate offline
    await context.setOffline(true);

    // Navigate to cart – should display cached data
    await page.goto('https://shop.example.com/cart');
    await expect(page.locator('#cart-item')).toHaveText(/Product 42/);
    await expect(page.locator('#offline-notice')).toBeVisible();

    // Attempt checkout – should be blocked with inline message
    await page.click('#checkout-button');
    await expect(page.locator('#checkout-error')).toHaveText(/Cannot process checkout while offline/);

    // Go back online
    await context.setOffline(false);
    await page.reload(); // or wait for reconnection event

    // Now checkout should succeed
    await page.click('#checkout-button');
    await expect(page.locator('#order-confirmation')).toBeVisible();
    await expect(page.locator('#order-number')).toMatch(/ORD-\d{6}/);
  });
});

Explanation

6.3 Autonomous Exploration with SUSA (No Code)

If you prefer to let a tool discover offline scenarios automatically, SUSA’s agent can be pointed at an APK or a web URL and instructed to inject network loss at random intervals.


# Install the CLI (once)
pip install susatest-agent

# Run on an Android APK
susatest-agent run \
  --apk ./app-release.apk \
  --device-emulator pixel_4_api_33 \
  --network-loss 20 \   # introduce a 20‑second offline window every 2‑3 minutes
  --personas curious impatient elderly \
  --output ./susatest-report.json \
  --format junit   # for CI consumption

What happens under the hood

  1. The agent installs the app on the emulator/device.
  2. It starts exploring UI elements using a mix of curated heuristics and learned patterns from prior runs.
  3. At the scheduled intervals, it calls the device‑agent API to toggle airplane mode (Android) or uses Playwright’s setOffline for web targets.
  4. Each discovered flow (login, search, checkout) is annotated with a PASS/FAIL verdict based on observed crashes, ANRs, or UI‑blocking messages.
  5. After the run, SUSA generates Appium (Android) and Playwright (web) regression scripts that you can check into your repo and refine.

When to use this approach

> *Note:* SUSA is mentioned here only to illustrate how autonomous exploration can bootstrap offline testing; the remainder of the guide focuses on script‑based approaches that you control directly.

7. Integrating Offline Tests into CI/CD Pipelines

7.1 Generic CI Steps

StageActionExample (GitHub Actions)
Checkoutactions/checkout@v4- uses: actions/checkout@v4
Setup JDK / NodeInstall language runtime- uses: actions/setup-java@v3
- uses: actions/setup-node@v3
Install dependenciesmvn clean install or npm ci- run: mvn -B clean install
Start test environmentEmulator / device farm spin‑up- uses: reactivecircus/android-emulator-runner@v2
- uses: browserstack/github-actions@v1 (for real devices)
Run offline test suiteExecute framework runner- run: ./gradlew connectedAndroidTest
- run: npx playwright test
Collect artifactsScreenshots, logs, test reports- uses: actions/upload-artifact@v3
Publish resultsJUnit/XML to test summary- uses: dorny/test-reporter@v1

7.2 Handling Device‑Specific Commands in CI

7.3 Flaky‑Test Mitigation in CI

7.4 Example GitHub Actions Workflow (Android + Appium)


name: Offline UI Tests

on:
  push:
    branches: [ main ]
  pull_request:

jobs:
  offline-tests:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          distribution: temurin
          java-version: 17

      - name: Cache Gradle packages
        uses: actions/cache@v3
        with:
          path: ~/.gradle/caches
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
          restore-keys: ${{ runner.os }}-gradle-

      - name: Build debug APK
        run: ./gradlew assembleDebug

      - name: Start Android Emulator
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 33
          target: google_apis
          arch: x86_64
          avd-name: test_avd
          force-avd-creation: true
          emulator-options: -no-window -no-audio

      - wait-for-emulator: |
          ./adb wait-for-device shell getprop sys.boot_completed | while read -r line; do
            if [[ "$line" == *"1"* ]]; then break; fi
            sleep 5
          done

      - name: Install Appium server
        run: npm install -g appium

      - name: Start Appium
        run: appium &
        env:
          APPIUM_LOG_LEVEL: error

      - name: Wait for Appium
        run: |
          until curl -s http://localhost:4723/wd/hub/status | grep '"value":{"ready":true}'; do
            sleep 2
          done

      - name: Run Offline Test Suite
        env:
          EMULATOR_SERIAL: emulator-5554
        run: |
          ./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.network=offline

      - name: Collect Test Results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: offline-test-results
          path: app/build/outputs/androidTest-results/connected/

      - name: Publish JUnit Report
        if: always()
        uses: dorny/test-reporter@v1
        with:
          name: Offline Android Tests
          path: app/build/outputs/androidTest-results/connected/**/TEST-*.xml
          reporter: java-junit

Takeaway – The workflow shows how to spin up an emulator, start Appium, and run a Gradle task that passes a custom argument (network=offline) to select the offline test variants. Adjust the --network-loss flag or device‑farm API calls as needed for your stack.

8. Reporting, Metrics, and Continuous Improvement

8.1 What to Measure

MetricWhy It MattersHow to Capture
Test pass rate (offline vs. online)Indicates stability of offline handlingJUnit/XML test results
Mean time to detect (MTTD) regressionsFaster feedback reduces cost of defectsTimestamp difference between commit and first failing test
Flakiness index (retries needed)Highlights unstable locators or waitsCount of retries per test in CI logs
Coverage of offline scenariosEnsures you test all critical flowsMap of feature‑offline matrix; compute % of cells with at least one test
Device‑farm utilizationHelps optimize spendCloud provider usage reports

8.2 Dashboard Example (Grafana + Prometheus)

If you expose a /metrics endpoint from your test runner (e.g., using Prometheus client libraries), you can chart:

A simple panel showing “Offline test pass rate over time” quickly reveals whether a recent change degraded offline resilience.

8.3 Using Test Artifacts for Debugging

8.4 Continuous Improvement Loop

  1. Review failures – triage each offline failure: is it a genuine bug, a flaky wait, or missing test‑id?
  2. Update locators – replace brittle selectors with stable IDs.
  3. Refactor waits – replace static sleeps with condition‑based waits.
  4. Add missing scenarios – extend the matrix (e.g., add a test for “offline while submitting form”).
  5. Commit and tag – label the commit with test-offline-improvement to track progress in changelogs.

Over time, you’ll see the flakiness index drop and the pass rate climb, giving confidence that offline mode is robust.

9. Checklist and Takeaways

9.1 Pre‑Flight Checklist (Run Before Adding a New Offline Test)

✅ ItemDescription
Network‑control verifiedConfirm that the command to toggle airplane mode or set latency works on the target device/emulator.
Test‑id presentEvery UI element asserted in the test has a stable resource-id, accessibility-id, or data-testid.
Deterministic dataCache or mock data is loaded before the test goes offline; no reliance on live server state.
Explicit wait usedAll assertions are wrapped in an explicit wait (no Thread.sleep or await timeout).
Teardown restores connectivityAn afterEach/finally block guarantees the device/emulator ends online.
Artifacts capturedScreenshot/video on failure is enabled and stored as an artifact.
CI job definedThe test is included in the relevant pipeline stage and reports JUnit/XML.
Flakiness guardRetry logic limited to ≤2 attempts; persistent failures trigger a ticket.
Peer reviewAt least one other engineer reviewed the test for clarity and maintainability.

9.2 Core Takeaways

  1. Start with the matrix – list every critical user flow and the network states you need to validate (online, offline, intermittent, throttled). This matrix drives test creation and prevents gaps.
  2. Pick a framework that gives you programmatic network control – Appium, Espresso/XCTest, Playwright, or Cypress all expose APIs to simulate loss; avoid relying on manual toggles in the test script.
  3. Invest in stable locators and explicit waits – the majority of flaky offline tests stem from timing issues or brittle selectors; fixing these pays dividends across the whole suite.
  4. Automate teardown – always revert to an online state after each test, even when the test fails, to keep the test farm in a known state.
  5. Leverage autonomous exploration for a seed suite – tools like SUSA can generate the first batch of offline tests, which you then refine and maintain.
  6. Integrate early and often – run offline tests on every pull request; the fast feedback loop prevents regressions from reaching staging or production.
  7. Measure and improve – track pass rate, flakiness, and MTTD; use the data to prioritize locator refactors and wait‑strategy updates.

By following the steps outlined here—starting with a clear test matrix, selecting the right framework, building resilient tests, wiring them into CI, and continuously measuring outcomes—you’ll transform offline mode from a vague, manually‑checked afterthought into a reliable, automated safety net that ships with every release. Happy testing!

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free