Markers control which tests run, which skip, and which retry on failure. Combined with a clear naming convention, they let you run targeted subsets — smoke on PRs, regression nightly, platform-specific suites on demand.
Registering markers
Always register markers in pytest.ini to avoid PytestUnknownMarkWarning:
[pytest]
markers =
smoke: core functionality, PR gate
regression: full suite, nightly
slow: tests over 60 seconds, excluded from PR checks
android_only: Android-specific features
ios_only: iOS-specific features
no_retry: tests that must not be retried (state-changing operations)Applying markers
import pytest
from pages.login_page import LoginPage
@pytest.mark.smoke
@pytest.mark.regression
def test_standard_user_login(driver):
home = LoginPage(driver).login("standard_user", "secret_sauce")
assert home.get_product_count() > 0
@pytest.mark.regression
@pytest.mark.slow
def test_performance_glitch_user(driver):
# This user has intentional delays — takes 10+ seconds
home = LoginPage(driver).login("performance_glitch_user", "secret_sauce")
assert home.is_displayed()
@pytest.mark.android_only
def test_android_notification_shade(android_driver):
android_driver.open_notifications()
...
@pytest.mark.ios_only
def test_ios_haptic_feedback(ios_driver):
...Running by marker
# PR gate — smoke tests only
pytest -m smoke
# Nightly — full regression, excluding very slow tests
pytest -m "regression and not slow"
# Android-specific tests
pytest -m android_only
# Anything except slow tests
pytest -m "not slow"Skipping tests conditionally
import sys
import pytest
@pytest.mark.skipif(sys.platform != "darwin", reason="iOS requires macOS")
def test_ios_specific_feature(ios_driver):
...
# Skip based on environment variable
@pytest.mark.skipif(
not os.getenv("BROWSERSTACK_ACCESS_KEY"),
reason="BrowserStack credentials not configured"
)
def test_on_real_device(driver):
...Expected failures
Mark tests that are known to fail (upstream bug, known flake) without failing the build:
@pytest.mark.xfail(reason="Known bug: APP-1234 — cart count doesn't update after removal")
def test_cart_count_after_removal(driver):
cart = add_item_to_cart(driver)
cart.remove_item("Sauce Labs Backpack")
assert cart.get_item_count() == 0 # currently fails
@pytest.mark.xfail(strict=True, reason="Should fail until fix is deployed")
def test_strict_xfail(driver):
# strict=True: if the test PASSES, report it as XPASS (unexpected pass) — fail the build
...Retry plugin (pytest-rerunfailures)
pip install pytest-rerunfailures# Retry each failing test up to 2 times
pytest --reruns 2
# Only retry when specific errors occur
pytest --reruns 2 --reruns-delay 1 --only-rerun "TimeoutException" --only-rerun "NoSuchElementException"Apply retry per-test with the mark:
@pytest.mark.flaky(reruns=2, reruns_delay=1)
def test_network_dependent_feature(driver):
...No-retry marker
For state-changing tests that must never retry (order placement, account creation), combine a custom marker with a conftest hook:
# pytest.ini
markers =
no_retry: must not be retried — creates real state
# conftest.py
def pytest_collection_modifyitems(items, config):
"""Remove rerunfailures from tests marked no_retry."""
for item in items:
if item.get_closest_marker("no_retry"):
item.add_marker(pytest.mark.flaky(reruns=0), append=False)@pytest.mark.no_retry
def test_place_order(driver):
# This creates a real order — retry would create duplicates
confirmation = checkout_flow(driver)
assert confirmation.get_order_id().startswith("ORD-")Marker-based test selection in CI
# .github/workflows/mobile.yml
jobs:
pr-smoke:
if: github.event_name == 'pull_request'
steps:
- run: pytest -m smoke
nightly-regression:
if: github.event_name == 'schedule'
steps:
- run: pytest -m "regression and not slow"
weekly-full-suite:
if: github.event_name == 'schedule' && github.event.schedule == '0 2 * * 0'
steps:
- run: pytest # run everything including slow testsPytest conftest.py organisation for large suites
For suites with 100+ tests across multiple features, split conftest.py by feature area:
tests/
conftest.py # driver, common fixtures
login/
conftest.py # login-specific fixtures
test_login.py
checkout/
conftest.py # checkout-specific fixtures (cart_with_item, etc.)
test_checkout.py
products/
conftest.py
test_products.py
pytest loads conftest files hierarchically — root fixtures are available everywhere; subdirectory fixtures are only available in that subtree.