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.
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.loggeroutput flows into the report automaticallypytest-plugins— CI reporting layer that works alongside the requirements manifest in pipeline workflowspytest-depends-on— run a specific dependency chain in isolation without triggering unrelated testspytest-reporter-html— isolated runs produce tight, single-test HTML reports that are easy to sharepytest-dynamic-parameterize— isolate one parameterized variant without running the full parameter setpytest-collect-requirements— collect test requirements from the same dynamic sources as your parameters for fully data-driven pipelines
Links
Built together with Yevgeny Farber. If your team is still debugging CI failures from terminal output, this is the upgrade.