BrowserStack App Automate Integration from Python

6 min read

BrowserStack provides real Android and iOS devices in the cloud. Connecting a Python Appium suite requires changing the server URL, adding BrowserStack-specific capabilities, and uploading the app — no test logic changes needed.

Authentication

BrowserStack uses username + access key in the server URL:

import os
 
USERNAME = os.environ["BROWSERSTACK_USERNAME"]
ACCESS_KEY = os.environ["BROWSERSTACK_ACCESS_KEY"]
BROWSERSTACK_URL = f"https://{USERNAME}:{ACCESS_KEY}@hub-cloud.browserstack.com/wd/hub"

Never hardcode credentials. Use environment variables set as GitHub Secrets, GitLab CI variables, or your CI system's secret store.

Uploading the app

Before running tests, upload the APK or IPA to BrowserStack's App Automate service:

import requests
import os
 
def upload_app_to_browserstack(app_path: str) -> str:
    """Upload app file and return the bs:// URL."""
    username = os.environ["BROWSERSTACK_USERNAME"]
    access_key = os.environ["BROWSERSTACK_ACCESS_KEY"]
 
    with open(app_path, "rb") as f:
        response = requests.post(
            "https://api-cloud.browserstack.com/app-automate/upload",
            files={"file": f},
            auth=(username, access_key)
        )
    response.raise_for_status()
    return response.json()["app_url"]  # bs://abc123...
 
 
# Or via shell before running tests:
# curl -u "$BS_USER:$BS_KEY" -X POST .../upload -F "file=@app.apk" | jq -r '.app_url'

BrowserStack capabilities

from appium.options import UiAutomator2Options
 
def create_browserstack_android_driver():
    options = UiAutomator2Options()
    options.device_name = "Samsung Galaxy S23"
    options.platform_version = "13.0"
    options.app = os.environ["BROWSERSTACK_APP_URL"]  # bs://...
 
    # BrowserStack-specific options
    options.load_capabilities({
        "bstack:options": {
            "projectName": "Mobile Regression Suite",
            "buildName": f"Build {os.environ.get('BUILD_NUMBER', 'local')}",
            "sessionName": "Android Login Test",
            "networkLogs": True,
            "deviceLogs": True,
            "video": True,
            "appiumVersion": "2.0.1",
        }
    })
 
    from appium import webdriver
    return webdriver.Remote(BROWSERSTACK_URL, options=options)

BrowserStack fixture

# conftest.py
import pytest
 
USE_BROWSERSTACK = os.getenv("BROWSERSTACK", "false").lower() == "true"
 
 
@pytest.fixture
def driver(request):
    if USE_BROWSERSTACK:
        d = create_browserstack_android_driver()
    else:
        d = create_local_android_driver()
 
    d.implicitly_wait(0)  # Disable implicit wait — use explicit waits
    yield d
 
    try:
        d.quit()
    except Exception:
        pass
# Local run
pytest tests/
 
# BrowserStack run
BROWSERSTACK=true BROWSERSTACK_APP_URL=bs://abc123 pytest tests/

Marking test status on BrowserStack

BrowserStack marks sessions as passed/failed based on whether the session ended cleanly. Override this with JavaScript to add a reason:

from selenium.webdriver.common.by import By
 
 
def mark_browserstack_session(driver, status: str, reason: str):
    """status: 'passed' or 'failed'"""
    driver.execute_script(
        'browserstack_executor: {"action": "setSessionStatus", '
        f'"arguments": {{"status": "{status}", "reason": "{reason}"}}}}'
    )

In conftest.py:

@pytest.fixture
def driver(request):
    d = create_driver()
    yield d
 
    if USE_BROWSERSTACK:
        if hasattr(request.node, "rep_call"):
            if request.node.rep_call.failed:
                reason = str(request.node.rep_call.longrepr)[:500]
                mark_browserstack_session(d, "failed", reason)
            else:
                mark_browserstack_session(d, "passed", "Test passed")
 
    d.quit()

Device matrix in pytest

Use fixture parametrisation to run against multiple devices:

DEVICE_MATRIX = [
    {"device": "Samsung Galaxy S23", "version": "13.0", "name": "S23_Android13"},
    {"device": "Google Pixel 7",     "version": "13.0", "name": "Pixel7_Android13"},
    {"device": "iPhone 15",          "version": "17",   "name": "iPhone15_iOS17"},
]
 
 
@pytest.fixture(params=DEVICE_MATRIX, ids=lambda d: d["name"])
def bs_driver(request):
    device_config = request.param
    # Build options based on device_config["device"] / ["version"]
    # ... (iOS or Android based on device name)
    d = create_browserstack_driver(device_config)
    yield d
    d.quit()
 
 
def test_login_on_all_devices(bs_driver):
    home = LoginPage(bs_driver).login("standard_user", "secret_sauce")
    assert home.get_product_count() > 0

With -n 3, all three devices run in parallel.

Accessing video recordings

BrowserStack retains session videos at:

https://app-automate.browserstack.com/builds/<BUILD_ID>/sessions/<SESSION_ID>

The session ID is available from the driver:

session_id = driver.session_id
bs_url = f"https://app-automate.browserstack.com/sessions/{session_id}"
print(f"BrowserStack session: {bs_url}")

Attach this URL to Allure reports for direct access to the video:

import allure
allure.dynamic.link(bs_url, name="BrowserStack Session")

// tip to track lessons you complete and pick up where you left off across devices.