DEV Community

Cover image for Migrate BrowserStack to DeviceLab: Appium
Om Narayan
Om Narayan

Posted on • Originally published at devicelab.dev on

Migrate BrowserStack to DeviceLab: Appium

Your Appium tests work. You've spent months building a reliable test suite. It runs against local devices without issues.

Then you moved to BrowserStack.

Suddenly you're dealing with browserstack.yml, bstack:options, app upload APIs, capability generators, and vendor-specific configurations. Your clean test code now has BrowserStack scattered throughout.

Here's how to undo all of that.

What BrowserStack Made You Do

Option A: The SDK Approach

BrowserStack's "recommended" approach requires a browserstack.yml file:

# browserstack.yml
userName: YOUR_USERNAME
accessKey: YOUR_ACCESS_KEY
framework: testng
app: bs://j3c874f21852ba57957a3fdc33f47514288c4ba4

platforms:
  - platformName: android
    deviceName: Samsung Galaxy S23
    platformVersion: '13.0'
  - platformName: android
    deviceName: Google Pixel 8
    platformVersion: '14.0'

parallelsPerPlatform: 2
browserstackLocal: true

buildName: Regression Suite
projectName: MyApp Android

debug: true
networkLogs: true
deviceLogs: true
appiumLogs: true
video: true

browserStackLocalOptions:
  forcelocal: true
  localIdentifier: randomstring
Enter fullscreen mode Exit fullscreen mode

Plus their SDK installed in your project:

<!-- pom.xml -->
<dependency>
    <groupId>com.browserstack</groupId>
    <artifactId>browserstack-java-sdk</artifactId>
    <version>1.13.3</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Plus environment variables:

export BROWSERSTACK_USERNAME="your_username"
export BROWSERSTACK_ACCESS_KEY="your_access_key"
Enter fullscreen mode Exit fullscreen mode

Option B: Direct Capabilities

If you're not using the SDK, your test code looks like this:

public class BrowserStackTest {
    private AndroidDriver driver;

    @BeforeMethod
    public void setUp() throws Exception {
        DesiredCapabilities capabilities = new DesiredCapabilities();

        // Standard Appium capabilities
        capabilities.setCapability("platformName", "android");
        capabilities.setCapability("appium:platformVersion", "13.0");
        capabilities.setCapability("appium:deviceName", "Samsung Galaxy S23");
        capabilities.setCapability("appium:automationName", "UiAutomator2");

        // BrowserStack-specific capabilities
        HashMap<String, Object> browserstackOptions = new HashMap<>();
        browserstackOptions.put("userName", "YOUR_USERNAME");
        browserstackOptions.put("accessKey", "YOUR_ACCESS_KEY");
        browserstackOptions.put("appiumVersion", "2.4.1");
        browserstackOptions.put("projectName", "MyApp");
        browserstackOptions.put("buildName", "Build 1.0");
        browserstackOptions.put("sessionName", "Login Tests");
        browserstackOptions.put("debug", true);
        browserstackOptions.put("networkLogs", true);
        browserstackOptions.put("video", true);
        capabilities.setCapability("bstack:options", browserstackOptions);

        // App uploaded to BrowserStack
        capabilities.setCapability("app", "bs://j3c874f21852ba57957a3fdc33f47514288c4ba4");

        // BrowserStack's hub URL
        driver = new AndroidDriver(
            new URL("https://hub-cloud.browserstack.com/wd/hub"),
            capabilities
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Before you can run tests, you also need to upload your app:

curl -u "YOUR_USERNAME:YOUR_ACCESS_KEY" \
  -X POST "https://api-cloud.browserstack.com/app-automate/upload" \
  -F "file=@/path/to/app.apk"

# Response:
# {"app_url":"bs://j3c874f21852ba57957a3fdc33f47514288c4ba4"}
Enter fullscreen mode Exit fullscreen mode

Then reference that bs:// hash in your capabilities. Every time you update your app, you need a new hash.


What DeviceLab Requires

Your Test Code

Same as your local test code. Just comment out the app capability:

public class DeviceLabTest {
    private AndroidDriver driver;

    @BeforeMethod
    public void setUp() throws Exception {
        DesiredCapabilities capabilities = new DesiredCapabilities();

        // Same capabilities as local testing
        capabilities.setCapability("platformName", "Android");
        capabilities.setCapability("appium:automationName", "UiAutomator2");

        // Comment out app capability - DeviceLab handles it via CLI
        // capabilities.setCapability("app", "./app.apk");

        // Connect to localhost - same as local
        driver = new AndroidDriver(
            new URL("http://localhost:4723/wd/hub"),
            capabilities
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

That's it. Same code you use locally. Comment one line. No SDK. No bstack:options. No app upload API.

Running Tests

# Terminal 1: Start DeviceLab test node
curl -fsSL https://app.devicelab.dev/test-node/KEY | sh -s -- \
  --framework appium \
  --app ./app.apk

# Wait for: ✅ Appium server ready on http://localhost:4723

# Terminal 2: Run your tests
mvn clean test
Enter fullscreen mode Exit fullscreen mode

DeviceLab handles app transfer to the device, app installation, Appium server setup, and port tunneling to localhost:4723.

You just run your tests against localhost like you always did locally.


Side-by-Side Comparison

Capabilities

Capability BrowserStack DeviceLab
platformName Yes Yes
automationName Yes Yes
deviceName Yes No (selected via CLI)
platformVersion Yes No
app Yes (bs://hash, upload first) No (passed via CLI)
bstack:options Yes (10+ keys) No
userName/accessKey Yes No (in org key)

Server URL

BrowserStack DeviceLab
URL https://hub-cloud.browserstack.com/wd/hub http://localhost:4723/wd/hub

Same URL you use for local testing.

Files Changed

BrowserStack DeviceLab
browserstack.yml Yes No
pom.xml (SDK) Add dependency No changes
Test code Add 15+ lines of caps Remove app capability
CI/CD Configure secrets One secret (org key)

The Migration

Step 1: Remove BrowserStack Dependencies

pom.xml:

<!-- DELETE THIS -->
<dependency>
    <groupId>com.browserstack</groupId>
    <artifactId>browserstack-java-sdk</artifactId>
    <version>1.13.3</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Delete:

  • browserstack.yml
  • Any BrowserStack-specific configuration files

Step 2: Clean Up Capabilities

Before (BrowserStack):

DesiredCapabilities capabilities = new DesiredCapabilities();
capabilities.setCapability("platformName", "android");
capabilities.setCapability("appium:platformVersion", "13.0");
capabilities.setCapability("appium:deviceName", "Samsung Galaxy S23");
capabilities.setCapability("appium:automationName", "UiAutomator2");

HashMap<String, Object> browserstackOptions = new HashMap<>();
browserstackOptions.put("userName", "YOUR_USERNAME");
browserstackOptions.put("accessKey", "YOUR_ACCESS_KEY");
browserstackOptions.put("appiumVersion", "2.4.1");
browserstackOptions.put("projectName", "MyApp");
browserstackOptions.put("buildName", "Build 1.0");
browserstackOptions.put("sessionName", "Login Tests");
browserstackOptions.put("debug", true);
browserstackOptions.put("networkLogs", true);
browserstackOptions.put("video", true);
capabilities.setCapability("bstack:options", browserstackOptions);

capabilities.setCapability("app", "bs://j3c874f21852ba57957a3fdc33f47514288c4ba4");

driver = new AndroidDriver(
    new URL("https://hub-cloud.browserstack.com/wd/hub"),
    capabilities
);
Enter fullscreen mode Exit fullscreen mode

After (DeviceLab):

DesiredCapabilities capabilities = new DesiredCapabilities();
capabilities.setCapability("platformName", "Android");
capabilities.setCapability("appium:automationName", "UiAutomator2");

driver = new AndroidDriver(
    new URL("http://localhost:4723/wd/hub"),
    capabilities
);
Enter fullscreen mode Exit fullscreen mode

Lines of capability code: BrowserStack: 22 lines → DeviceLab: 6 lines

Step 3: Update CI/CD

Before (GitHub Actions with BrowserStack):

name: Mobile Tests
on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Upload App to BrowserStack
        run: |
          APP_URL=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \
            -X POST "https://api-cloud.browserstack.com/app-automate/upload" \
            -F "file=@app/build/outputs/apk/debug/app-debug.apk" \
            | jq -r '.app_url')
          echo "APP_URL=$APP_URL" >> $GITHUB_ENV

      - name: Run Tests
        env:
          BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
          BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
          APP_URL: ${{ env.APP_URL }}
        run: mvn clean test
Enter fullscreen mode Exit fullscreen mode

After (GitHub Actions with DeviceLab):

name: Mobile Tests
on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Start DeviceLab
        run: |
          curl -fsSL https://app.devicelab.dev/test-node/${{ secrets.DEVICELAB_ORG_KEY }} | sh -s -- \
            --framework appium \
            --app ./app/build/outputs/apk/debug/app-debug.apk &
          sleep 30  # Wait for Appium server

      - name: Run Tests
        run: mvn clean test
Enter fullscreen mode Exit fullscreen mode

Secrets needed: BrowserStack: 2 → DeviceLab: 1


What You Gain

1. No More "Works Locally, Fails in Cloud" Debugging

This is where teams lose the most time with BrowserStack.

The BrowserStack reality:

Your staging environment is behind a firewall. You need to open firewall rules to BrowserStack IPs (security risk), or run BrowserStack Local tunnel on every CI machine. Tunnel drops mid-test, causing random failures. DNS resolution works differently in their cloud. SSL certificates fail for internal services. Your staging server rate-limits BrowserStack IPs.

Result: Teams spend more time debugging environment issues than actual test failures. The test isn't flaky—the connection to your staging API is.

The DeviceLab reality:

Your devices sit on your network. They already have access to your staging servers, internal APIs, VPNs, and test databases. Same access as your laptop.

No firewall changes. No tunnels to configure or maintain. No "works on my machine" debugging. Same network, same DNS, same access.

If your test works locally, it works on DeviceLab.

2. Your Data Stays on Your Network

BrowserStack:

Your APK → BrowserStack servers → Their devices → Logs in their cloud
Enter fullscreen mode Exit fullscreen mode

DeviceLab:

Your APK → Your device (via WebRTC P2P) → Results on your machine
Enter fullscreen mode Exit fullscreen mode

DeviceLab never sees your app, your test data, or your credentials. We route the connection; we never see the content.

3. No App Upload Dance

Every time you update your app with BrowserStack:

# Upload new build
curl -u "user:key" -X POST ".../upload" -F "file=@new-app.apk"
# Get new hash: bs://abc123
# Update capabilities or browserstack.yml with new hash
# Or use custom_id and hope it picks the right build
Enter fullscreen mode Exit fullscreen mode

With DeviceLab:

# Just pass the new APK
curl ... --app ./new-app.apk
Enter fullscreen mode Exit fullscreen mode

No hashes. No upload API. No wondering if you're testing the right build.

4. Faster Feedback

BrowserStack flow: Upload APK (30s-2min depending on size) → Wait for processing → Queue for device → Run test

DeviceLab flow: APK transfers directly to your device → Run test

Your devices. No queue. No shared infrastructure.

5. Test on Your Actual Devices

BrowserStack gives you devices in their data center. They're real, but they're not your devices.

DeviceLab lets you test on:

  • The exact devices your users have
  • Devices with your company's MDM profiles
  • Devices on your network with your backend access
  • That weird Android 8 phone your CEO insists on using

6. Any Appium Version, Any Plugin

BrowserStack: Limited to Appium versions 1.21.0 through 2.19.0. The current Appium release is 3.1.2. No custom plugins allowed.

DeviceLab: You control the Appium server. Run version 3.1.2, pin to 2.x for stability, or use any version you need. Install any Appium plugins—images, gestures, or your own custom plugins.

Your test infrastructure, your rules.


Device Selection

BrowserStack:

capabilities.setCapability("appium:deviceName", "Samsung Galaxy S23");
capabilities.setCapability("appium:platformVersion", "13.0");
Enter fullscreen mode Exit fullscreen mode

DeviceLab:

# Specific device
curl ... --device-names "Samsung Galaxy S23"

# Multiple devices (parallel)
curl ... --device-count 5

# Any available device
curl ...  # DeviceLab picks one
Enter fullscreen mode Exit fullscreen mode

Device selection moves from code to CLI. Your test code stays clean.


Parallel Testing

BrowserStack:

# browserstack.yml
parallelsPerPlatform: 5
platforms:
  - deviceName: Samsung Galaxy S23
    platformVersion: '13.0'
  - deviceName: Google Pixel 8
    platformVersion: '14.0'
Enter fullscreen mode Exit fullscreen mode

DeviceLab:

curl ... --device-count 5
Enter fullscreen mode Exit fullscreen mode

Same result. Less configuration.


Common Migration Issues

"I need video recordings and logs"

DeviceLab captures test artifacts locally by default. You can optionally enable cloud storage in settings, but no logs are uploaded unless you choose to.

"I have tests spread across multiple files with different capabilities"

With DeviceLab, capabilities are minimal. Just ensure all tests point to http://localhost:4723/wd/hub and don't set the app capability.

"What about iOS?"

Same approach. DeviceLab supports iOS devices:

curl -fsSL https://app.devicelab.dev/test-node/KEY | sh -s -- \
  --framework appium \
  --platform ios \
  --app ./MyApp.ipa
Enter fullscreen mode Exit fullscreen mode

Your XCUITest-based Appium tests work the same way.


Migration Checklist

Step Action
1 Remove browserstack-java-sdk from pom.xml
2 Delete browserstack.yml
3 Remove all bstack:options from capabilities
4 Remove app capability (or leave blank)
5 Change server URL to http://localhost:4723/wd/hub
6 Update CI/CD to use DeviceLab test node
7 Connect your devices with device node
8 Run tests

Total time: 30 minutes if your test code is well-organized.


Set Up Your Devices (Once)

Before running tests, connect your devices to DeviceLab:

# On the machine with your devices
curl -fsSL https://app.devicelab.dev/device-node/KEY | sh
Enter fullscreen mode Exit fullscreen mode

Devices appear in your dashboard. They're now available for testing from anywhere.


Summary

BrowserStack DeviceLab
Setup time Hours (SDK, caps, yml) Minutes
Code changes 20+ lines of capabilities Remove 1 line
App handling Upload API → bs:// hash --app flag
Server URL Their hub URL localhost:4723
Appium version 1.21.0–2.19.0 only Any (including 3.x)
Plugins Not allowed Any plugin
Your data On their servers Never leaves your network
Your devices Theirs (shared) Yours (owned)

Your tests already work. Stop adapting them for someone else's platform.

Try DeviceLab

Top comments (0)