Reusable Appium 2.x + pytest mobile test framework
Project description
appium-pytest-kit
appium-pytest-kit is a reusable Appium 2.x + pytest plugin library for Python 3.11+. Install it once, generate a .env, and start writing mobile tests with zero boilerplate.
pip install appium-pytest-kit
appium-pytest-kit-init --framework --root my-project
Full documentation: DOCUMENTATION.md · docs/
What it gives you
| Zero-config fixtures | driver, waiter, actions, page_factory — just add to your test function |
| Auto failure artifacts | Screenshot + page source + device logs captured automatically on failure |
| 3-tier device resolution | explicit settings → named profile → auto-detect via adb/xcrun |
| Session modes | clean (per-test) · clean-session (shared) · debug (keep alive) |
| Retry support | Session reused across retry attempts — no restart cost between tries |
| Fail-fast | --app-fail-fast stops the suite after retries are exhausted, not before |
| Explicit waits | WaitTimeoutError with structured .locator and .timeout context |
| High-level actions | tap, type, swipe, scroll, assertions — all wait-safe |
| Page + flow objects | Scaffold generates pages/ and flows/ with base classes ready to extend |
| Extension hooks | Override settings, inject capabilities, run code after driver creation |
| CLI scaffold | One command to generate a full project structure |
Dependencies
All required dependencies are installed automatically with pip install appium-pytest-kit. You do not need a separate requirements.txt.
| Auto-installed | Version | Purpose |
|---|---|---|
Appium-Python-Client |
≥ 4.0.0 | Appium WebDriver client |
pydantic-settings |
≥ 2.3.0 | .env and env var loading |
pytest |
≥ 8.2.0 | Test runner integration |
Optional extras (install only what you need):
pip install "appium-pytest-kit[yaml]" # device profile YAML support
pip install "appium-pytest-kit[allure]" # Allure report attachments
pip install "appium-pytest-kit[retry]" # pytest-retry for flaky test handling
pip install "appium-pytest-kit[all]" # all optional extras
| Extra | Installs | When you need it |
|---|---|---|
[yaml] |
PyYAML ≥ 6.0 | Named device profiles in data/devices.yaml |
[allure] |
allure-pytest ≥ 2.13.0 | Screenshots + page source in Allure reports |
[retry] |
pytest-retry ≥ 0.6.0 | Retry flaky tests while reusing the same Appium session |
Installation
From PyPI
pip install appium-pytest-kit
From GitHub
pip install git+https://github.com/gianlucasoare/appium-pytest-kit.git
Local clone (development)
git clone https://github.com/gianlucasoare/appium-pytest-kit.git
cd appium-pytest-kit
pip install -e ".[dev]"
Quickstart: test an app in 5 minutes
1 — Scaffold the project
pip install appium-pytest-kit
appium-pytest-kit-init --framework --root my-project
cd my-project
2 — Edit .env with your device and app
APP_PLATFORM=android
APP_APPIUM_URL=http://127.0.0.1:4723
APP_APP_PACKAGE=com.example.myapp
APP_APP_ACTIVITY=.MainActivity
APP_DEVICE_NAME=emulator-5554
APP_PLATFORM_VERSION=14
3 — Start Appium and your emulator, then run
appium &
pytest tests/android/test_smoke.py -v
4 — Write a real test
# tests/android/test_login.py
import pytest
from appium.webdriver.common.appiumby import AppiumBy
USERNAME = (AppiumBy.ID, "com.example.app:id/username")
PASSWORD = (AppiumBy.ID, "com.example.app:id/password")
LOGIN_BTN = (AppiumBy.ACCESSIBILITY_ID, "login_button")
WELCOME = (AppiumBy.ID, "com.example.app:id/welcome_text")
@pytest.mark.integration
def test_login(actions):
actions.type_text(USERNAME, "testuser")
actions.type_text(PASSWORD, "secret")
actions.tap(LOGIN_BTN)
assert actions.text(WELCOME) == "Welcome, testuser"
pytest -m integration -v
Built-in fixtures
| Fixture | Scope | Description |
|---|---|---|
settings |
session | Resolved AppiumPytestKitSettings |
device_info |
session | Resolved device (name, UDID, version) |
appium_server |
session | Server URL, optional lifecycle management |
driver |
function | Live appium.webdriver.Remote, auto-quit |
waiter |
function | Explicit waits with WaitTimeoutError |
actions |
function | High-level UI helpers |
page_factory |
function | Factory for page objects: page_factory(LoginPage) |
Page objects with page_factory
# pages/login_page.py
from appium.webdriver.common.appiumby import AppiumBy
from appium_pytest_kit import Locator
from pages.base_page import BasePage
class LoginPage(BasePage):
_USERNAME: Locator = (AppiumBy.ID, "com.example.app:id/username")
_LOGIN_BTN: Locator = (AppiumBy.ACCESSIBILITY_ID, "login_button")
def log_in(self, username: str, password: str) -> None:
self._actions.type_text(self._USERNAME, username)
self._actions.tap(self._LOGIN_BTN)
def is_loaded(self) -> bool:
return self._actions.is_displayed(self._USERNAME)
# tests/test_login.py
def test_login_success(page_factory):
login = page_factory(LoginPage)
login.wait_until_loaded()
login.log_in("testuser", "secret")
# ...
See docs/page-objects.md for the full guide.
Session modes
APP_SESSION_MODE=clean # fresh driver per test (default)
APP_SESSION_MODE=clean-session # one shared driver for the whole run (faster)
APP_SESSION_MODE=debug # shared + no restart on failure (local debugging)
Retry support
Install the extra, then use @pytest.mark.flaky(...) and/or the --retries CLI flag:
pip install "appium-pytest-kit[retry]"
# Retry this test up to 2 extra times (3 total attempts)
@pytest.mark.flaky(retries=2)
def test_flaky_animation(actions):
actions.tap(START_BTN)
actions.assert_displayed(RESULT_SCREEN)
# Retry every failed test up to 2 extra times, stop if something is truly broken
pytest --retries 2 --retry-delay 1 --app-fail-fast
How it works: during retries the same Appium session is reused — no restart between attempts. Once the test passes or all retries are exhausted, the session is quit and the next test starts fresh.
See docs/cli-reference.md for the full retry flag reference.
Device resolution (3-tier)
- Explicit —
APP_DEVICE_NAME/APP_UDIDin.envor CLI - Profile —
APP_DEVICE_PROFILE=pixel7fromdata/devices.yaml - Auto-detect —
adb devices(Android) orxcrun simctl/xctrace(iOS)
pytest --app-device-profile pixel7
pytest --app-udid emulator-5554
pytest # auto-detect if nothing set
Failure diagnostics
On test failure the framework automatically captures:
- Screenshot →
artifacts/screenshots/<test_id>.png - Page source →
artifacts/pagesource/<test_id>.xml - Device logs →
artifacts/device_logs/<test_id>.log - Video (if configured) →
artifacts/videos/<test_id>.mp4
APP_VIDEO_POLICY=failed # record and save only on failure
APP_VIDEO_POLICY=always # record every test
Allure attachments are added automatically when allure-pytest is installed.
Configuration
Settings load from .env → env vars → CLI flags (highest wins).
pytest --app-platform ios
pytest --app-device-name "Pixel 7" --app-platform-version 14
pytest --app-appium-url http://192.168.1.10:4723
pytest --app-session-mode clean-session
pytest --app-device-profile pixel7
pytest --app-video-policy failed
pytest --app-override APP_EXPLICIT_WAIT_TIMEOUT=15
pytest --app-capabilities-json '{"autoGrantPermissions": true}'
pytest --app-strict-config
pytest --app-manage-appium-server
pytest --app-reporting-enabled
# Retry support (requires appium-pytest-kit[retry])
pytest --retries 2 --retry-delay 1 # retry all tests up to 2 extra times
pytest --retries 2 --app-fail-fast # stop suite after retries are exhausted
See docs/configuration.md for all settings.
Extension hooks
# conftest.py
def pytest_appium_pytest_kit_capabilities(capabilities, settings):
"""Add extra capabilities before each driver session."""
if settings.platform == "android":
return {"autoGrantPermissions": True, "language": "en"}
def pytest_appium_pytest_kit_configure_settings(settings):
"""Replace settings at session start."""
return settings.model_copy(update={"explicit_wait_timeout": 20.0})
def pytest_appium_pytest_kit_driver_created(driver, settings):
"""Run setup immediately after each driver is created."""
driver.orientation = "PORTRAIT"
Expanded waits
waiter.for_clickable(locator)
waiter.for_invisibility(locator)
waiter.for_text_contains(locator, "partial text")
waiter.for_text_equals(locator, "exact text")
waiter.for_all_visible([loc1, loc2, loc3]) # single timeout for the whole group
waiter.for_all_gone([loc1, loc2])
waiter.for_any_visible([loc1, loc2])
waiter.for_context_contains("WEBVIEW")
waiter.for_android_activity("MainActivity")
Expanded actions
# Tap
actions.tap_if_present(locator)
actions.tap_if_present_first_available([l1, l2])
actions.tap_by_coordinates(x, y)
actions.double_tap(locator)
actions.long_press(locator, duration_seconds=2)
# Text
actions.type_if_present(locator, "text")
actions.type_text_slowly(locator, "otp", delay_per_char=0.15)
actions.clear(locator)
# Visibility assertions
actions.is_displayed(locator)
actions.assert_displayed(locator)
actions.is_not_displayed(locator)
actions.assert_not_displayed(locator)
actions.assert_displayed_first_available([l1, l2])
actions.assert_not_displayed_first_available([l1, l2])
# Text assertions
actions.assert_text(locator, "exact text")
actions.assert_text_contains(locator, "partial")
actions.assert_text_not_empty(locator)
# Attribute assertion
actions.assert_attribute(locator, "checked", "true")
# Enabled/disabled state
actions.is_enabled(locator)
actions.assert_enabled(locator)
actions.assert_not_enabled(locator)
# Checked/selected state (checkboxes, toggles)
actions.is_checked(locator)
actions.assert_checked(locator)
actions.assert_not_checked(locator)
# Element count
actions.count(locator) # → int
actions.assert_count(locator, 3)
# Scroll
actions.scroll_down()
actions.scroll_to_element(locator)
# Keyboard
actions.hide_keyboard()
actions.press_keycode(66) # ENTER
# App lifecycle
actions.activate_app("com.example.myapp")
actions.terminate_app("com.example.myapp")
actions.background_app(2)
actions.open_deep_link("myapp://profile", app_id="com.example.myapp")
# Hybrid
actions.switch_to_webview()
actions.switch_to_native()
Public API
from appium_pytest_kit import (
AppiumPytestKitSettings,
AppiumPytestKitError,
ConfigurationError, DeviceResolutionError, LaunchValidationError,
WaitTimeoutError, ActionError, DriverCreationError,
DeviceInfo, DriverConfig, MobileActions, Waiter,
Locator, # type alias: tuple[str, str]
build_driver_config, create_driver, load_settings, apply_cli_overrides,
)
Fixture lifecycle
flowchart TD
A["pytest start"] --> B["load defaults + .env + env vars"]
B --> C["apply --app-* CLI overrides"]
C --> D["settings fixture (session)"]
D --> E{"APP_MANAGE_APPIUM_SERVER"}
E -->|"true"| F["start local Appium server"]
E -->|"false"| G["use APP_APPIUM_URL"]
F --> H["appium_server fixture (session)"]
G --> H
H --> I{"session_mode"}
I -->|"clean-session / debug"| J["_driver_shared (session)"]
I -->|"clean"| K["driver per test"]
J --> K
K --> L["waiter / actions / page_factory"]
K --> M["test runs"]
M --> N{"failed?"}
N -->|"yes"| O["capture screenshot + page source"]
N --> P["stop video (per policy)"]
O --> P
P --> Q["driver.quit() (clean mode)"]
Q --> R["report summary flush"]
R --> S["server stop (if managed)"]
Debug logs
appium-pytest-kit logs every action, wait, and session lifecycle event using Python's standard logging module. Enable them with a single pytest flag:
pytest --log-cli-level=INFO # session lifecycle + artifacts
pytest --log-cli-level=DEBUG # full trace (every tap, wait, scroll)
Or persist in pyproject.toml:
[tool.pytest.ini_options]
log_cli = true
log_cli_level = "INFO"
See docs/troubleshooting.md for a full table of log messages.
Local development
pip install -e ".[dev]"
python -m ruff check .
python -m pytest -q
python -m pytest --collect-only examples/basic/tests -q
Documentation
| Topic | File |
|---|---|
| Installation + dependencies | docs/installation.md |
| Project structure + scaffold | docs/project-structure.md |
| Configuration (all settings) | docs/configuration.md |
| CLI reference (all flags) | docs/cli-reference.md |
| Built-in fixtures | docs/fixtures.md |
| Page objects guide | docs/page-objects.md |
| conftest.py guide | docs/conftest-guide.md |
| Waits reference | docs/waits.md |
| Actions reference | docs/actions.md |
| Session modes | docs/session-modes.md |
| Device resolution | docs/device-resolution.md |
| Failure diagnostics + video | docs/diagnostics.md |
| Error reference | docs/errors.md |
| Troubleshooting | docs/troubleshooting.md |
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file appium_pytest_kit-0.1.4.tar.gz.
File metadata
- Download URL: appium_pytest_kit-0.1.4.tar.gz
- Upload date:
- Size: 40.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d73adc5141b6b55eba53f075d3bcb91d14cfa58fd46e7aa258080325a62fc715
|
|
| MD5 |
e5140e841094bbbf2851273d2913580e
|
|
| BLAKE2b-256 |
d0c057c9769f84e0b6c40cea218ece0c4df9c374f160be2584b15d6146632e29
|
File details
Details for the file appium_pytest_kit-0.1.4-py3-none-any.whl.
File metadata
- Download URL: appium_pytest_kit-0.1.4-py3-none-any.whl
- Upload date:
- Size: 40.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d061633036c7e890bba4cc8d950e8a32562b1815213779366af1bac139076460
|
|
| MD5 |
681d4767a500f9dcbde60f8fab776c6d
|
|
| BLAKE2b-256 |
889831da71bb2b23bd6d0c0662fb2decbfdf5fd10cf1a60900df928e32646e7e
|