Allure Report with pytest: Beautiful Python Test Reports

Allure Report with pytest: Beautiful Python Test Reports

allure-pytest is the pytest plugin for Allure Report. Use @allure.step, @allure.epic, @allure.feature, and allure.attach to annotate your Python tests and generate interactive HTML reports with history, attachments, and step details.

Key Takeaways

Install allure-pytest and allure-commandline. The Python package generates results; the Java tool generates HTML.

@allure.step and with allure.step() document test flow. Steps appear in the report as collapsible sections showing what the test did.

allure.attach adds files to the report. Attach screenshots, API responses, log files, and any binary content.

@allure.epic, @allure.feature, @allure.story organize tests. Use these to structure the report hierarchically by business domain.

@allure.severity marks test importance. Filter the report to show only critical failures after a release.

Setup

pip install allure-pytest

# Install allure commandline (Java required)
# macOS
brew install allure

# Or download from GitHub releases
curl -L https://github.com/allure-framework/allure2/releases/download/2.28.0/allure-2.28.0.tgz | tar -xz
export PATH=$PATH:$(pwd)/allure-2.28.0/bin

Basic Usage

# test_user_api.py
import allure
import pytest
import requests

@allure.epic("User Management")
@allure.feature("User Registration")
class TestUserRegistration:

    @allure.story("Successful Registration")
    @allure.severity(allure.severity_level.CRITICAL)
    def test_register_with_valid_data(self, api_base_url):
        with allure.step("Send registration request"):
            response = requests.post(f"{api_base_url}/users", json={
                "name": "Alice Smith",
                "email": "alice@example.com",
                "password": "SecurePass123!"
            })

        with allure.step("Verify 201 Created response"):
            assert response.status_code == 201

        with allure.step("Verify user ID in response"):
            data = response.json()
            assert "id" in data
            assert data["name"] == "Alice Smith"

        allure.attach(
            response.text,
            name="Registration Response",
            attachment_type=allure.attachment_type.JSON
        )

    @allure.story("Duplicate Email")
    @allure.severity(allure.severity_level.NORMAL)
    def test_register_duplicate_email_fails(self, api_base_url):
        with allure.step("Create first user"):
            requests.post(f"{api_base_url}/users", json={
                "name": "Alice Smith",
                "email": "duplicate@example.com",
                "password": "Pass123!"
            })

        with allure.step("Attempt duplicate registration"):
            response = requests.post(f"{api_base_url}/users", json={
                "name": "Another Alice",
                "email": "duplicate@example.com",
                "password": "Pass123!"
            })

        with allure.step("Verify 409 Conflict"):
            assert response.status_code == 409
            assert "already exists" in response.json().get("error", "")

Using @allure.step as a Decorator

For reusable helper functions:

@allure.step("Login as {username}")
def login(base_url: str, username: str, password: str) -> dict:
    response = requests.post(f"{base_url}/auth/login", json={
        "username": username,
        "password": password
    })
    assert response.status_code == 200
    return response.json()

@allure.step("Add item {item_id} to cart")
def add_to_cart(base_url: str, token: str, item_id: str) -> dict:
    response = requests.post(f"{base_url}/cart/items",
        json={"itemId": item_id},
        headers={"Authorization": f"Bearer {token}"}
    )
    assert response.status_code == 200
    return response.json()

def test_purchase_flow(api_base_url):
    auth = login(api_base_url, "alice@example.com", "password")
    cart = add_to_cart(api_base_url, auth["token"], "ITEM-001")
    assert cart["quantity"] == 1

Step parameters ({username}, {item_id}) are substituted in the report with actual values.

Attaching Files and Screenshots

import allure

def test_search_results(browser, base_url):
    browser.get(f"{base_url}/search?q=laptop")

    # Attach page screenshot
    allure.attach(
        browser.get_screenshot_as_png(),
        name="Search Results",
        attachment_type=allure.attachment_type.PNG
    )

    # Attach page source
    allure.attach(
        browser.page_source,
        name="Page HTML",
        attachment_type=allure.attachment_type.HTML
    )

    results = browser.find_elements(By.CSS_SELECTOR, ".search-result")
    assert len(results) > 0

Attach from file:

with open("response_body.json") as f:
    allure.attach(f.read(), "API Response", allure.attachment_type.JSON)

Conftest Setup for Automatic Failure Screenshots

# conftest.py
import pytest
import allure

@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    result = outcome.get_result()

    if result.when == "call" and result.failed:
        # Attach fixture data from the test
        browser = item.funcargs.get("browser")
        if browser:
            allure.attach(
                browser.get_screenshot_as_png(),
                name="Failure Screenshot",
                attachment_type=allure.attachment_type.PNG
            )

Running Tests and Generating Reports

# Run tests with allure results output
pytest tests/ --alluredir=allure-results

# Run specific tests
pytest tests/test_checkout.py -v --alluredir=allure-results

# Generate HTML report
allure generate allure-results --clean -o allure-report

# Open report
allure open allure-report

# Or one command
allure serve allure-results

Parameterized Tests

@pytest.mark.parametrize("status_code,expected_message", [
    (400, "Bad Request"),
    (401, "Unauthorized"),
    (403, "Forbidden"),
    (404, "Not Found"),
    (500, "Internal Server Error"),
])
@allure.story("HTTP Error Handling")
def test_error_messages(api_base_url, status_code, expected_message):
    allure.dynamic.title(f"HTTP {status_code} returns '{expected_message}'")

    response = requests.get(f"{api_base_url}/simulate-error/{status_code}")
    assert response.status_code == status_code
    assert expected_message in response.json().get("message", "")

allure.dynamic.title() sets a meaningful title per parameter combination in the report.

@allure.issue("https://jira.example.com/browse/SHOP-123", "SHOP-123")
@allure.testcase("https://testlab.example.com/cases/TC-456", "TC-456")
@allure.story("Payment Processing")
def test_payment_gateway():
    # ...

These appear as clickable links in the report.

CI Integration

GitHub Actions:

- name: Run pytest with Allure
  run: pytest tests/ --alluredir=allure-results
  continue-on-error: true

- name: Generate Allure Report
  if: always()
  run: allure generate allure-results --clean -o allure-report

- name: Upload Allure report
  if: always()
  uses: actions/upload-artifact@v4
  with:
    name: allure-report
    path: allure-report/

Quick Reference

Annotation Purpose
@allure.epic("name") Top-level business domain
@allure.feature("name") Feature within an epic
@allure.story("name") User story
@allure.severity(level) blocker, critical, normal, minor, trivial
@allure.step("name") Function-level step
with allure.step("name"): Block-level step
allure.attach(data, name, type) Add attachment
@allure.issue(url, name) Link to issue tracker
@allure.testcase(url, name) Link to test case
allure.dynamic.title(name) Set title at runtime

Allure with pytest turns your test results from a terminal output into a navigable report that product managers and QA leads can actually use.

Read more

Start now free