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
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>
Plus environment variables:
export BROWSERSTACK_USERNAME="your_username"
export BROWSERSTACK_ACCESS_KEY="your_access_key"
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
);
}
}
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"}
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
);
}
}
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
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>
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
);
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
);
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
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
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
DeviceLab:
Your APK → Your device (via WebRTC P2P) → Results on your machine
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
With DeviceLab:
# Just pass the new APK
curl ... --app ./new-app.apk
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");
DeviceLab:
# Specific device
curl ... --device-names "Samsung Galaxy S23"
# Multiple devices (parallel)
curl ... --device-count 5
# Any available device
curl ... # DeviceLab picks one
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'
DeviceLab:
curl ... --device-count 5
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
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
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.
Top comments (0)