Test Skipping and Expected Failures

This module provides the functionality to selectively skip tests or mark them as expected to fail (xfail). It enables conditional test skipping and xfail marking based on runtime conditions, facilitating flexible test suite management and improved test reporting.


Core Concepts and Purpose


How the Module Works

1. Configuration and Command-Line Options

def pytest_addoption(parser: Parser) -> None:
    group.addoption(
        "--runxfail",
        action="store_true",
        dest="runxfail",
        default=False,
        help="Report the results of xfail tests as if they were not marked",
    )
    parser.addini(
        "xfail_strict",
        "Default for the strict parameter of xfail markers when not given explicitly (default: False)",
        default=False,
        type="bool",
    )

2. Markers for Skipping and Xfail

These markers can be applied in test code, enabling declarative control over test execution.

3. Evaluation of Skip and Xfail Conditions

def evaluate_condition(item: Item, mark: Mark, condition: object) -> tuple[bool, str]:
    # Evaluates string or boolean conditions, returns (result, reason)
    ...
def evaluate_skip_marks(item: Item) -> Skip | None:
    # Returns Skip if skip/skipif condition is met
    ...
def evaluate_xfail_marks(item: Item) -> Xfail | None:
    # Returns Xfail if xfail condition is met
    ...

4. Hook Implementations for Test Lifecycle Integration

@hookimpl(tryfirst=True)
def pytest_runtest_setup(item: Item) -> None:
    skipped = evaluate_skip_marks(item)
    if skipped:
        raise skip.Exception(skipped.reason, _use_item_location=True)

    item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item)
    if xfailed and not item.config.option.runxfail and not xfailed.run:
        xfail("[NOTRUN] " + xfailed.reason)

@hookimpl(wrapper=True)
def pytest_runtest_call(item: Item) -> Generator[None]:
    xfailed = item.stash.get(xfailed_key, None)
    if xfailed is None:
        item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item)

    if xfailed and not item.config.option.runxfail and not xfailed.run:
        xfail("[NOTRUN] " + xfailed.reason)

    try:
        return (yield)
    finally:
        xfailed = item.stash.get(xfailed_key, None)
        if xfailed is None:
            item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item)

@hookimpl(wrapper=True)
def pytest_runtest_makereport(
    item: Item, call: CallInfo[None]
) -> Generator[None, TestReport, TestReport]:
    rep = yield
    xfailed = item.stash.get(xfailed_key, None)
    if item.config.option.runxfail:
        pass  # don't interfere
    elif call.excinfo and isinstance(call.excinfo.value, xfail.Exception):
        rep.wasxfail = call.excinfo.value.msg
        rep.outcome = "skipped"
    elif not rep.skipped and xfailed:
        if call.excinfo:
            raises = xfailed.raises
            if raises is None or (
                (
                    isinstance(raises, (type, tuple))
                    and isinstance(call.excinfo.value, raises)
                )
                or (
                    isinstance(raises, AbstractRaises)
                    and raises.matches(call.excinfo.value)
                )
            ):
                rep.outcome = "skipped"
                rep.wasxfail = xfailed.reason
            else:
                rep.outcome = "failed"
        elif call.when == "call":
            if xfailed.strict:
                rep.outcome = "failed"
                rep.longrepr = "[XPASS(strict)] " + xfailed.reason
            else:
                rep.outcome = "passed"
                rep.wasxfail = xfailed.reason
    return rep

5. Reporting Test Status for Skipped and Xfail Tests

def pytest_report_teststatus(report: BaseReport) -> tuple[str, str, str] | None:
    if hasattr(report, "wasxfail"):
        if report.skipped:
            return "xfailed", "x", "XFAIL"
        elif report.passed:
            return "xpassed", "X", "XPASS"
    return None

Module Interactions and Relationships

This module's functionality is integrated tightly with the pytest test lifecycle hooks (`pytest_runtest_setup`, `pytest_runtest_call`, `pytest_runtest_makereport`), enabling it to influence test execution, control skipping, and shape reporting.


Important Concepts and Design Patterns


Mermaid Sequence Diagram: Skipping and Xfail Evaluation and Reporting Flow

sequenceDiagram
    participant Config as Config & CLI Options
    participant Item as Test Item
    participant Setup as pytest_runtest_setup
    participant Call as pytest_runtest_call
    participant Report as pytest_runtest_makereport
    participant Outcome as Test Outcome Reporting

    Config->>Item: Provide config (runxfail, xfail_strict)
    Item->>Setup: Evaluate skip marks
    alt skip condition true
        Setup->>Setup: Raise skip.Exception (skip test)
        Setup-->>Outcome: Mark test as skipped
    else
        Item->>Setup: Evaluate xfail marks
        alt xfail with run=False and not runxfail
            Setup->>Setup: Trigger xfail (no run)
            Setup-->>Outcome: Mark test as xfail skipped
        else
            Setup-->>Call: Allow test run
            Call->>Call: Yield to test function
            Call-->>Report: Capture call result or exception
        end
    end

    Report->>Item: Retrieve xfail info from stash
    alt call raised xfail.Exception
        Report->>Outcome: Mark test as xfail skipped
    else if call raised expected exception or no exception
        Report->>Outcome: Mark test as xfail skipped or passed
    else if call raised unexpected exception or unexpected pass with strict
        Report->>Outcome: Mark test failed or xpassed with failure
    end

This detailed explanation covers the purpose, functionality, integration, and key design elements of the Test Skipping and Expected Failures module, emphasizing its role in controlling test execution flow and improving test suite robustness and reporting.