Allure with pytest — allure-pytest Integration

7 min read

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-pytest

Also install the Allure CLI for report generation:

# macOS
brew install allure
 
# Or via npm
npm install -g allure-commandline

Running 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-results

Annotating 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-results

To auto-generate reports on GitHub Pages, use the allure-report-action or generate the report in a separate job after test collection.

// tip to track lessons you complete and pick up where you left off across devices.