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() > 0With -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")