Parallel Execution with pytest-xdist

6 min read

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-xdist

Running 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.py

The 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:
        pass

The 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 devices

In 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() == product

With -n 4, each worker runs one product test simultaneously — 4 products in ~1 test's worth of time.

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