Search code examples
pythonpytestpytest-html

Adding new field to TestReport object and using it in pytest_html_results_table_html hook


I want to get information about exceptions raised during test call phase and add them to report created with pytest-html plugin. So I created hookwrapper for pytest_runtest_makereport:

@hookimpl(hookwrapper=True)
def pytest_runtest_makereport(call):
    outcome = yield
    result = outcome.get_result()
    errors = getattr(result, "errors", [])
    if result.when == "call":
        if result.failed:
            if error := call.excinfo:
                errors.append(error.typename)
                result.errors = errors
        else:
            logger.info("PASSED")

To add this information to test report, I'm using:

def pytest_html_results_table_html(report, data):
    del data[:]
    if errors := getattr(report, "errors", []):
        data.append(html.div(", ".join(errors), class_="failed log"))

Unfortunately in pytest_html_results_table_html there is no errors field in report instance. If I add result.errors = errors to teardown phase, field appears in report object but it has empty list. I know there is an extra field, but pytest-html adds it straight into report. I want to do something with those values before adding them.

So what I'm missing here? How to pass this value from pytest_runtest_makereport to pytest_html_results_table_html?

Example test class that I'm using:

from logging import getLogger

from pytest import fixture

logger = getLogger()


class TestVariousOutputs:
    @fixture()
    def setup(self):
        logger.info("Run setup")
        raise RuntimeError("Error raised during setup")

    @fixture()
    def teardown(self):
        logger.info("Run setup")
        yield
        logger.info("Run teardown")
        raise RuntimeError("Error raised during teardown")

    def test_pass(self):
        logger.info("Run test")
        assert True

    def test_fail(self):
        logger.info("Run test")
        assert False, "Assert False"

    def test_raise_error(self):
        logger.info("Run test")
        raise RuntimeError("Error raised during test run")

    def test_setup_raise_error(self, setup):
        logger.info("Run test")
        assert True

    def test_teardown_raise_error(self, teardown):
        logger.info("Run test")
        assert True

    def test_teardown_raise_error_and_test_fails(self, teardown):
        logger.info("Run test")
        assert False, "Assert False but teardown should also fail"


Solution

  • Because this is how pytest-html is currently implemented: for each test case, it will take the teardown report and copy selected fields from the call and setup reports. Of course, custom fields are simply ignored (duh!). You thus have to intercept this post-processing of pytest reports and ensure your custom field is present on teardown report. Example to put into your conftest.py:

    @pytest.hookimpl(tryfirst=True)  # run our hookimpl before pytest-html does its own postprocessing
    def pytest_sessionfinish(session):
        html_report = getattr(session.config, "_html", None)
        for rep_setup, rep_call, rep_teardown in html_report.reports.values():
            # copy errors field from call report to teardown report
            rep_teardown.errors = getattr(rep_call, "errors", [])