Post

pytest-reporter-html: Rich, Interactive HTML Reports for pytest — Zero Config

pytest-reporter-html generates rich, interactive HTML test reports with zero-config log capture, named step tracking, exception rendering, and real-time filtering — open the file in any browser and start debugging.

pytest-reporter-html: Rich, Interactive HTML Reports for pytest — Zero Config

There’s a moment every QA or automation engineer knows well: a test fails in CI, the logs are a wall of text, and you spend more time finding the failure than fixing it. Not a raw log dump. Not a terminal screenshot pasted into Slack. Something you can open in a browser, filter by status, expand the failure, and immediately understand what went wrong.

Why I Built This

I’ve built a lot of test infrastructure over the years — pytest plugins, CI integrations, custom reporters — and the one thing every team I’ve worked with has asked for eventually is a readable test report. The existing options were either too basic (pytest-html) or too heavy. Together with Yevgeny Farber, I built the reporter I actually wanted to use: fully interactive, zero code changes to get started, and structured around how test failures are actually debugged.

Installation

1
pip install pytest-reporter-html
1
uv add pytest-reporter-html

Quick Start

Add --report-html to your pytest options:

1
2
3
4
5
# pytest.ini
[pytest]
addopts =
    --report-html
    --output-dir=logs

After the run, open the report:

1
logs/test-reports/TestReport_Latest.html

No fixtures, no code changes, no config files to maintain. The plugin attaches to Python’s root logger automatically — every logging.*() call your tests already make is captured and surfaced in the report without any wiring.

Real-World Example

Here’s an API test with named steps. The report turns a flat log stream into a structured story about what the test actually did:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from custom_python_logger import get_logger
from pytest_reporter_html import step

logger = get_logger(__name__)

def test_user_lifecycle():
    with step("Create user"):
        logger.info("Creating a new user with role 'user'")
        response = api.post("/users", json={"role": "user"})
        assert response.status_code == 201

    with step("Update profile"):
        logger.info("Updating user's role to 'admin'")
        response = api.patch(f"/users/{user_id}", json={"role": "admin"})
        assert response.status_code == 200

    with step("Verify changes"):
        logger.info("Verifying that the role change persisted")
        response = api.get(f"/users/{user_id}")
        assert response.json()["role"] == "admin"

The report renders this as:

1
2
3
4
Test: test_user_lifecycle                                            PASSED
 ├── Step 01: Create user                               PASSED    120ms
 ├── Step 02: Update profile                            PASSED     45ms
 └── Step 03: Verify changes                            PASSED     30ms

When a step fails, the report auto-expands that test and highlights the failing step. The assertion message, stack trace, and every log event leading up to the failure are visible in context — no grepping through logs required.

Key Features

Zero-config log capture was a non-negotiable design decision. Every team already has logging in their tests. By attaching to the root logger, the plugin works with logging.info(...), custom_python_logger, structlog, or anything else that feeds the standard logging chain — no fixture swaps, no special calls.

Named steps are the main quality-of-life feature. The step context manager wraps any block of code into a collapsible, timed section in the report. Each step shows its duration and pass/fail status at a glance. Without manual steps, Setup, test body, and Teardown are created automatically for every test. step also works as a decorator — both sync and async:

1
2
3
4
5
6
7
@step("Fetch user data")
def get_user(user_id: str) -> dict:
    return api.get(f"/users/{user_id}").json()

@step("Send notification")
async def notify(user_id: str) -> None:
    await notification_service.send(user_id)

The HTML report ships with live search, status filters (Passed / Failed), and log-level filters (TRACE → ERROR) — all client-side, no server required. The report is a single self-contained file you can share via Slack, attach to a Jira ticket, or archive in S3 without deploying anything.

JSON content is syntax-highlighted inline. HTTP requests are rendered with a generated cURL command and copy button — which matters a lot when debugging API test failures and you want to reproduce a specific request outside the test.

Per-test JSON files are written alongside the HTML, one per test. If you want to post results to a dashboard, run a flakiness analysis, or build an alert on repeated failures, the structured JSON gives you the raw data independently of the HTML rendering. The report doesn’t have to be a dead end.

Goes Well With

  • custom-python-logger — the logger used in the examples; self.logger output flows into the report automatically
  • pytest-plugins — CI reporting layer that works alongside the requirements manifest in pipeline workflows
  • pytest-depends-on — run a specific dependency chain in isolation without triggering unrelated tests
  • pytest-reporter-html — isolated runs produce tight, single-test HTML reports that are easy to share
  • pytest-dynamic-parameterize — isolate one parameterized variant without running the full parameter set
  • pytest-collect-requirements — collect test requirements from the same dynamic sources as your parameters for fully data-driven pipelines

Built together with Yevgeny Farber. If your team is still debugging CI failures from terminal output, this is the upgrade.

This post is licensed under CC BY 4.0 by the author.