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:
passStep 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 -vExpected:
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 -vExpected: 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-resultsVerify 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.