Guided Walkthrough — POM, pytest Suite, Allure Report

9 min read

This walkthrough builds the complete login test file from scratch. Follow along step by step, then apply the same pattern to the product and checkout test files.

Step 1: Verify connectivity

Before writing any page objects, confirm the driver creates a session and finds one element:

# test_connectivity.py (throw away after this step)
def test_session_creates(driver):
    from appium.webdriver.common.appiumby import AppiumBy
    username_field = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "test-Username")
    assert username_field.is_displayed(), "Username field not visible"
    print(f"Current activity: {driver.current_activity}")

Run with pytest test_connectivity.py -s. If this passes, the driver, capabilities, and Appium connection work. Fix any failures here before proceeding.

Step 2: Inspect element IDs

Open Appium Inspector with the same server URL and capabilities. On the Sauce Labs login screen, find:

  • Username field: accessibility id = "test-Username"
  • Password field: accessibility id = "test-Password"
  • Login button: accessibility id = "test-LOGIN"
  • Error message container: accessibility id = "test-Error message"

These become the locator constants in LoginPage.

Step 3: Build LoginPage

# pages/login_page.py
from appium.webdriver.common.appiumby import AppiumBy
from pages.base_page import BasePage
 
 
class LoginPage(BasePage):
    USERNAME_FIELD = (AppiumBy.ACCESSIBILITY_ID, "test-Username")
    PASSWORD_FIELD = (AppiumBy.ACCESSIBILITY_ID, "test-Password")
    LOGIN_BUTTON   = (AppiumBy.ACCESSIBILITY_ID, "test-LOGIN")
    ERROR_MESSAGE  = (AppiumBy.ACCESSIBILITY_ID, "test-Error message")
 
    def login(self, username: str, password: str) -> "HomePage":
        self.clear_and_type(self.USERNAME_FIELD, username)
        self.clear_and_type(self.PASSWORD_FIELD, password)
        self.driver.hide_keyboard()
        self.tap(self.LOGIN_BUTTON)
        from pages.home_page import HomePage
        return HomePage(self.driver)
 
    def get_error_message(self) -> str:
        return self.wait_for_visible(self.ERROR_MESSAGE).text
 
    def is_error_visible(self) -> bool:
        return self.is_visible(self.ERROR_MESSAGE, timeout=3)

clear_and_type calls element.clear() then element.send_keys() — it's defined in BasePage. hide_keyboard() dismisses the soft keyboard before tapping Login, preventing the keyboard from covering the button.

Step 4: Build the driver fixture

# conftest.py
import os
import pytest
from appium import webdriver
from appium.options import UiAutomator2Options
 
 
@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)
 
 
@pytest.fixture(scope="function")
def driver(request):
    options = UiAutomator2Options()
    options.device_name = os.getenv("ANDROID_DEVICE", "emulator-5554")
    options.app = os.path.abspath("apps/saucelabs.apk")
    options.auto_grant_permissions = True
    options.no_reset = False  # Fresh app state for each test
 
    d = webdriver.Remote(
        os.getenv("APPIUM_SERVER", "http://127.0.0.1:4723"),
        options=options
    )
    yield d
 
    if hasattr(request.node, "rep_call") and request.node.rep_call.failed:
        os.makedirs("screenshots", exist_ok=True)
        d.get_screenshot_as_file(f"screenshots/{request.node.name}.png")
 
    try:
        d.quit()
    except Exception:
        pass

Step 5: Write the login tests

# tests/test_login.py
import pytest
import allure
from pages.login_page import LoginPage
 
 
@allure.epic("Authentication")
@allure.feature("Login")
class TestLogin:
 
    @pytest.mark.smoke
    @pytest.mark.regression
    @allure.story("Standard user")
    @allure.severity(allure.severity_level.BLOCKER)
    def test_standard_user_login(self, driver):
        home = LoginPage(driver).login("standard_user", "secret_sauce")
        assert home.get_product_count() > 0
 
    @pytest.mark.regression
    @allure.story("Locked out user")
    @allure.severity(allure.severity_level.CRITICAL)
    def test_locked_out_user(self, driver):
        page = LoginPage(driver)
        page.login("locked_out_user", "secret_sauce")
        error = page.get_error_message()
        assert "locked out" in error.lower()
 
    @pytest.mark.regression
    @pytest.mark.parametrize("username,password,expected_fragment", [
        ("invalid_user",   "secret_sauce", "do not match"),
        ("standard_user",  "wrong_pass",   "do not match"),
        ("",               "",             "Username is required"),
        ("standard_user",  "",             "Password is required"),
    ], ids=["wrong_user", "wrong_pass", "empty_both", "no_password"])
    @allure.story("Validation errors")
    def test_invalid_credentials(self, driver, username, password, expected_fragment):
        page = LoginPage(driver)
        page.login(username, password)
        error = page.get_error_message()
        assert expected_fragment in error, \
            f"Expected '{expected_fragment}' in error '{error}' for user '{username}'"

Step 6: Build a minimal HomePage

The login tests return a HomePage — you need a minimal implementation so the login() call compiles:

# pages/home_page.py
from appium.webdriver.common.appiumby import AppiumBy
from pages.base_page import BasePage
 
 
class HomePage(BasePage):
    PRODUCT_TITLE = (AppiumBy.ACCESSIBILITY_ID, "test-PRODUCTS")
    PRODUCT_ITEMS = (AppiumBy.ACCESSIBILITY_ID, "test-Item")
 
    def __init__(self, driver):
        super().__init__(driver)
        # Verify we're on the home page
        self.wait_for_visible(self.PRODUCT_TITLE, timeout=10)
 
    def get_product_count(self) -> int:
        return len(self.find_all(self.PRODUCT_ITEMS))

The __init__ wait raises a TimeoutException if the login failed — giving you a clear "HomePage did not load" error instead of a confusing NoSuchElementException from a later assertion.

Step 7: Run the smoke suite

pytest tests/test_login.py -m smoke -v

Expected:

tests/test_login.py::TestLogin::test_standard_user_login PASSED   [100%]
1 passed in 18.43s

Then run all login tests:

pytest tests/test_login.py -v

Expected: 7 tests (1 smoke + 1 locked + 4 parametrized invalid + 1 empty), all passing.

Step 8: Generate the Allure report

pytest tests/test_login.py --alluredir=allure-results
allure serve allure-results

Verify in the report:

  • Epic "Authentication" > Feature "Login" visible in sidebar
  • Story breakdown: "Standard user", "Locked out user", "Validation errors"
  • Failed tests (if any) have screenshot attachment
  • Parametrized tests show individual IDs: [wrong_user], [wrong_pass], etc.

Apply the same pattern to test_products.py and test_checkout.py. The locator IDs for products start with test-Item and sort options use test-Modal Selector.

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