Allure turns pytest output into interactive HTML reports with screenshots, step-by-step history, and environment metadata. For mobile suites, it shows which tests failed on which device and what the screen looked like at failure.
Installing allure-pytest
pip install allure-pytestAlso install the Allure CLI for report generation:
# macOS
brew install allure
# Or via npm
npm install -g allure-commandlineRunning tests with Allure
# Generate raw results (JSON) in allure-results/
pytest --alluredir=allure-results
# Generate HTML report from results
allure generate allure-results -o allure-report --clean
# Open the report
allure open allure-report
# Or generate and serve in one command
allure serve allure-resultsAnnotating tests
import allure
import pytest
@allure.epic("Authentication")
@allure.feature("Login")
class TestLogin:
@allure.story("Standard user login")
@allure.severity(allure.severity_level.BLOCKER)
@allure.description("Verify standard user can log in and see the product catalog")
def test_standard_user_login(self, driver):
home = LoginPage(driver).login("standard_user", "secret_sauce")
assert home.get_product_count() > 0
@allure.story("Locked out user")
@allure.severity(allure.severity_level.CRITICAL)
def test_locked_out_user_sees_error(self, driver):
page = LoginPage(driver)
page.login("locked_out_user", "secret_sauce")
assert "locked out" in page.get_error_message()The Epic → Feature → Story hierarchy appears in Allure's left-hand navigation.
Adding steps
import allure
class LoginPage(BasePage):
@allure.step("Enter email: {email}")
def enter_email(self, email: str):
self.wait_for_clickable(self.EMAIL_FIELD).send_keys(email)
return self
@allure.step("Enter password")
def enter_password(self, password: str):
self.wait_for_clickable(self.PASSWORD_FIELD).send_keys(password)
return self
@allure.step("Tap Login button")
def tap_login(self):
self.wait_for_clickable(self.LOGIN_BUTTON).click()
from pages.home_page import HomePage
return HomePage(self.driver)
def login(self, email: str, password: str):
return self.enter_email(email).enter_password(password).tap_login(){email} in the step name is substituted with the actual argument value. Avoid {password} — it would appear in plain text in the report.
Attaching screenshots to Allure
import allure
from appium import webdriver
def attach_screenshot(driver: webdriver.Remote, name: str = "Screenshot"):
screenshot = driver.get_screenshot_as_png()
allure.attach(
screenshot,
name=name,
attachment_type=allure.attachment_type.PNG
)In conftest.py:
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
rep = outcome.get_result()
setattr(item, f"rep_{rep.when}", rep)
# Attach screenshot on failure
if rep.failed and call.when == "call":
driver = item.funcargs.get("driver")
if driver:
attach_screenshot(driver, "Screenshot at failure")item.funcargs is the dictionary of fixture values — driver is in there if the test uses the driver fixture.
Adding environment info
Create allure-results/environment.properties before running tests:
# conftest.py
import os
from pathlib import Path
def pytest_configure(config):
"""Write Allure environment properties."""
results_dir = Path("allure-results")
results_dir.mkdir(exist_ok=True)
env_file = results_dir / "environment.properties"
env_file.write_text(
f"Platform={os.getenv('PLATFORM', 'Android')}\n"
f"Device={os.getenv('ANDROID_DEVICE', 'emulator-5554')}\n"
f"AppiumVersion={get_appium_version()}\n"
f"PythonClient=Appium-Python-Client\n"
)
def get_appium_version() -> str:
try:
import subprocess
result = subprocess.run(["appium", "--version"], capture_output=True, text=True)
return result.stdout.strip()
except Exception:
return "unknown"Parametrize with Allure IDs
Give each parametrized case a stable Allure ID:
@pytest.mark.parametrize("username,password,expect_success", [
pytest.param("standard_user", "secret_sauce", True,
marks=allure.link("https://jira.example.com/APP-100", name="APP-100")),
pytest.param("locked_out_user", "secret_sauce", False,
marks=allure.issue("https://jira.example.com/APP-101", name="APP-101")),
])
def test_login(driver, username, password, expect_success):
...Attaching page source for element-not-found failures
def attach_page_source(driver: webdriver.Remote):
allure.attach(
driver.page_source,
name="Element Hierarchy (XML)",
attachment_type=allure.attachment_type.XML
)The page source is the same content as Appium Inspector — searchable for element IDs and attributes. Attaching it makes "element not found" failures debuggable from the report alone.
CI integration
# .github/workflows/mobile.yml
- name: Run tests
run: pytest --alluredir=allure-results
- name: Upload Allure results
uses: actions/upload-artifact@v4
if: always()
with:
name: allure-results-${{ github.run_number }}
path: allure-resultsTo auto-generate reports on GitHub Pages, use the allure-report-action or generate the report in a separate job after test collection.