pytest-xdist runs tests across multiple worker processes simultaneously. For mobile suites, this means driving multiple emulators or devices at the same time — Android tests in one process, iOS in another, or the same test suite split across multiple Android emulators.
Installing pytest-xdist
pip install pytest-xdistRunning tests in parallel
# 2 workers (processes)
pytest -n 2
# One worker per CPU core
pytest -n auto
# 4 workers for a specific test file
pytest -n 4 tests/test_products.pyThe isolation requirement
Each worker process runs independently. Fixtures in worker 1 and worker 2 run simultaneously. This means:
- Each worker needs its own driver (no shared driver objects)
- Each worker needs its own device (no shared Appium sessions)
- Port allocations must not collide
The driver fixture must be function-scoped (default) and create a new session per test. Session-scoped fixtures are dangerous with xdist — the session fixture runs once per worker, not once per entire run.
Assigning workers to specific devices
When running Android and iOS tests in parallel, assign each worker to a specific device:
# conftest.py
import pytest
import os
DEVICE_POOL = [
{"platform": "Android", "device": "emulator-5554", "wda_port": None},
{"platform": "iOS", "device": "iPhone 15", "wda_port": 8100},
]
def pytest_configure(config):
"""Register the worker_id fixture."""
pass
@pytest.fixture(scope="session")
def worker_device(worker_id):
"""Maps xdist worker to a specific device config."""
# worker_id is 'gw0', 'gw1', etc. — or 'master' when not using xdist
if worker_id == "master":
return DEVICE_POOL[0]
index = int(worker_id.replace("gw", "")) % len(DEVICE_POOL)
return DEVICE_POOL[index]worker_id is a built-in xdist fixture that returns the current worker's ID (gw0, gw1, etc.).
Complete parallel driver fixture
@pytest.fixture(scope="function")
def driver(worker_device):
platform = worker_device["platform"]
if platform == "Android":
from appium.options import UiAutomator2Options
options = UiAutomator2Options()
options.device_name = worker_device["device"]
options.app = os.path.abspath("apps/app.apk")
options.auto_grant_permissions = True
options.system_port = 8200 + int(worker_device["device"].replace("emulator-", "")) % 100
else:
from appium.options import XCUITestOptions
options = XCUITestOptions()
options.device_name = worker_device["device"]
options.app = os.path.abspath("apps/MyApp.app")
options.wda_local_port = worker_device["wda_port"]
from appium import webdriver
d = webdriver.Remote("http://127.0.0.1:4723", options=options)
yield d
try:
d.quit()
except Exception:
passThe system_port for Android (8200 + offset) prevents UiAutomator2 internal server port collisions when multiple Android sessions run simultaneously.
Running with explicit parallelism
# Android and iOS in parallel
pytest -n 2 tests/
# Android smoke (worker 0) and iOS smoke (worker 1)
pytest -n 2 -k "smoke"Disabling parallelism for specific tests
Some tests must run sequentially — tests that share a real device, tests that modify global state, or tests that measure timing:
@pytest.mark.xdist_group("serial")
def test_place_order(driver):
# This test should not run in parallel with other order tests
...Group multiple tests under the same group name — xdist runs them sequentially in the same worker:
@pytest.mark.xdist_group("serial")
def test_place_order(driver):
...
@pytest.mark.xdist_group("serial")
def test_verify_order_history(driver):
...Or force a test to always run in worker 0:
@pytest.mark.xdist_group(name="worker0")
def test_requires_specific_device(driver):
...Parallel emulator setup
Before running with -n 4, you need 4 emulators running:
# Start 4 Android emulators
emulator -avd Pixel_4_API_33 -port 5554 &
emulator -avd Pixel_4_API_33 -port 5556 &
emulator -avd Pixel_7_API_34 -port 5558 &
emulator -avd Pixel_7_API_34 -port 5560 &
# Wait for all to boot
adb wait-for-device # waits for at least one
# Verify
adb devicesIn CI, use the reactivecircus/android-emulator-runner GitHub Action for each emulator or pre-built Docker images with multiple AVDs.
Combining xdist with parametrize
Parametrized tests automatically distribute across workers:
@pytest.mark.parametrize("product", ["Backpack", "Bike Light", "Bolt T-Shirt", "Fleece Jacket"])
def test_product_detail(driver, product):
home = login(driver)
detail = home.tap_product(product)
assert detail.get_product_name() == productWith -n 4, each worker runs one product test simultaneously — 4 products in ~1 test's worth of time.