Custom Markers and Filtering for Mobile Test Suites

6 min read

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 tests

Pytest 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.

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