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/binBasic 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"] == 1Step 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) > 0Attach 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-resultsParameterized 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.
Links to Issues and Tests
@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.