Skip to main content

Collecting pytest markers from failing tests

You can annotate tests with markers, and using the pytest_collection_modifyitems and pytest_terminal_summary hooks you can get a list of markers for tests that failed.

I’m using pytest to monitor some of my websites, and when the tests fail, I want to produce a human-friendly report that describes which pages are down. Each test is checking a single website, and a single website may be checked by multiple tests. For example, I might have a test that checks a website is up, that it has the right HTTP headers, and that its HTTPS cert isn’t about to expire.

I wanted to annotate my tests with custom markers, so that I know which website is being checked by a particular test. For example, I could mark a test that’s checking my blog:

@pytest.mark.blog
def test_my_blog_is_up():
    resp = httpx.get("https://alexwlchan.net/")
    assert resp.status_code == 200

When the tests fail, I want to gather the markers from the failing tests, and use them to build my human-friendly report. I was able to do this with a couple of hooks that run at the end of the test.

Example

Here’s a simple test suite:

import pytest


@pytest.mark.falsehoods
def test_A():
    assert 0 == 1


@pytest.mark.truthiness
def test_B():
    assert 1 == 1

If I run this test, pytest will warn me about unknown markers. I can define the markers in my pytest.ini or pyproject.toml so pytest knows what they are:

[pytest]
markers =
    falsehoods: marks tests as describing falsehoods
    truthiness: marks tests as containing truthfulness

Then the important bit is in conftest.py, where I define two hooks:

import pytest

test_marks = {}


def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
    """
    Record the marks defined on each test item.
    """
    for item in items:
        test_marks[item.nodeid] = [mark.name for mark in item.iter_markers()]


def pytest_terminal_summary(
    terminalreporter: pytest.TerminalReporter,
    exitstatus: pytest.ExitCode,
    config: pytest.Config,
) -> None:
    """
    Print a list of marks on each test that failed.
    """
    terminalreporter.write_sep("-", "Failing test marks")

    for report in terminalreporter.getreports("failed"):
        nodeid = report.nodeid
        marks = test_marks.get(nodeid, [])
        terminalreporter.write_line(f"{nodeid} -> Marks: {marks}")

When the test starts, I use the pytest_collection_modifyitems hook to build a dict mapping test IDs to their markers. Although I have access to the markers later in report.keywords, they’re a bit easier to get here – the keywords list includes some stuff other than my markers.

When the tests are complete, I use the pytest_terminal_summary hook to print some extra text in the terminal report. In this example, I’m printing the name of each failing test and its markers:

my_test.py::test_A -> Marks: ['falsehoods']

You can obviously do more sophisticated stuff in this hook; this just shows you how to get the data. For example, in my uptime tests, I’m gathering a list of all the markers on any failing test and using them in a message that gets posted to Slack.