בדיקת Android APK בפרויקט אוטומציה עם Python ו-Appium
מסיבות של אבטחת מידע אני לא מציג כאן פרויקט אמיתי מלא, אבל כן אפשר להציג מבנה עבודה נכון.
בדוגמה הזאת מדובר בבדיקת Android APK: כלומר בדיקה של אפליקציה Native שמותקנת על Android Emulator או מכשיר Android אמיתי.
חשוב להבין: בניגוד ל-Android Chrome, כאן זו כבר לא בדיקת Web.
לכן לא משתמשים ב-CSS selectors, אלא בלוקייטורים Native כמו Accessibility ID.
באנדרואיד זה בדרך כלל מגיע מהערך contentDescription שהוגדר באפליקציה.
הפונקציה הראשית של הטסט
ב-Pytest כל פונקציה שמתחילה ב-test_ נחשבת לטסט.
במקרה הזה הפונקציה הראשית היא:
def test_android_apk_core_actions(android_apk):
"""Teach a longer Appium native Android flow that can be copied for real APKs."""
לפני שהקוד בתוך הפונקציה רץ, Pytest מכין את ה-fixture בשם:
android_apk
ה-fixture הזה יוצר Appium driver, מתקין או פותח את האפליקציה, ואז הטסט מתחיל לעבוד מול המסך של האפליקציה.
הבדל חשוב בין Android Chrome לבין Android APK
ב-Android Chrome אנחנו בודקים אתר, ולכן הלוקייטורים נראים בדרך כלל כך:
(by.CSS_SELECTOR, "[data-testid='email-input']")
אבל ב-Android APK אנחנו בודקים אפליקציה Native, ולכן הלוקייטור נראה כך:
(AppiumBy.ACCESSIBILITY_ID, "android-email-input")
כלומר:
- Android Chrome – אתר Web, משתמשים ב-CSS.
- Android APK – אפליקציה Native, משתמשים ב-Accessibility ID.
ייבוא הדברים שצריך
בתחילת הקובץ אני מייבא את הכלים הקבועים: Allure לדוחות, Pytest להרצת בדיקות,
AppiumBy ללוקייטורים Native, ו-WebDriverWait להמתנות יציבות.
# Import Allure decorators for report grouping.
import allure
# Import pytest to mark this test with environment labels.
import pytest
# Import AppiumBy for native Android locators.
from appium.webdriver.common.appiumby import AppiumBy
# Import Selenium expected conditions for explicit waits.
from selenium.webdriver.support import expected_conditions as ec
# Import WebDriverWait to wait for native app elements.
from selenium.webdriver.support.ui import WebDriverWait
# Import Allure helpers for readable steps and text attachments.
from wulfauto.core.allure_utils import allure_step, attach_text
נתוני בדיקה במקום אחד
כדי לא לערבב נתונים בתוך ה-flow, אני שם את נתוני המשתמש בחלק העליון של הקובץ.
בפרויקט אחר מחליפים כאן שם, אימייל, סוג תוכנית, טלפון או כל מידע עסקי אחר.
# Keep editable native Android test data near the top so this file is easy to copy.
TEST_USER = {
"name": "Android APK",
"email": "[email protected]",
"plan": "pro",
}
לוקייטורים Native במקום אחד
גם את הלוקייטורים אני שומר במקום אחד. במקרה של Native Android אני משתמש ב-Accessibility IDs.
כך אם באפליקציה משנים identifier, אני לא צריך לחפש אותו בכל הקובץ.
# Keep native Android accessibility IDs in one place so locator changes are easy to make.
AID = {
"title": "android-title",
"status": "android-status",
"start_button": "android-start-button",
"name_input": "android-name-input",
"email_input": "android-email-input",
"plan_pro": "android-plan-pro",
"terms_checkbox": "android-terms-checkbox",
"submit_button": "android-submit-button",
"deep_target": "android-deep-target",
}
פונקציית aid
במקום לכתוב בכל פעולה את אותו מבנה של Appium locator, אני משתמש בפונקציה קטנה שמחזירה tuple.
כלומר זוג ערכים: איך לחפש, ומה לחפש.
def aid(key: str) -> tuple[str, str]:
"""Return a native Android accessibility locator from the ID dictionary."""
# Native Android tests should prefer accessibility IDs when the app exposes them.
return (AppiumBy.ACCESSIBILITY_ID, AID[key])
לדוגמה:
aid("email_input")
מחזיר בפועל:
(AppiumBy.ACCESSIBILITY_ID, "android-email-input")
המתנות יציבות
באוטומציה לא כדאי ללחוץ מיד על אלמנט. צריך לחכות שהוא באמת מופיע או לחיץ.
לכן יש helper שמחזיר WebDriverWait:
def wait_for(driver, timeout: int = 10) -> WebDriverWait:
"""Create a WebDriverWait object for the current native Android session."""
# A helper keeps wait timeouts consistent across the file.
return WebDriverWait(driver, timeout)
ואז משתמשים בו בתוך פונקציות עזר:
def visible_native(driver, key: str, timeout: int = 10):
"""Wait until a native Android element is visible by accessibility ID."""
# Visibility waits make native UI actions more stable than immediate find_element calls.
return wait_for(driver, timeout).until(ec.visibility_of_element_located(aid(key)))
def clickable_native(driver, key: str, timeout: int = 10):
"""Wait until a native Android element is clickable by accessibility ID."""
# Clickability waits avoid tapping before Android finishes drawing the control.
return wait_for(driver, timeout).until(ec.element_to_be_clickable(aid(key)))
Tap באפליקציה Native
במקום להשתמש ישירות ב-click() בכל מקום, אני עוטף את הפעולה בפונקציית tap.
כך כל לחיצה כוללת גם המתנה.
def tap(driver, key: str, timeout: int = 10) -> None:
"""Tap a native Android element after waiting for it to be clickable."""
# This mirrors the web tap helper, but uses native accessibility IDs.
clickable_native(driver, key, timeout=timeout).click()
שימוש:
tap(android_apk, "start_button")
הכנסת טקסט לשדות Native
באפליקציה Native, הכנסת טקסט נעשית עם send_keys, אבל הלוקייטורים הם Native.
def type_text(driver, key: str, value: str, clear_first: bool = True, timeout: int = 10) -> None:
"""Type text into a native Android input by accessibility ID."""
# Wait until the input is visible before interacting with it.
field = visible_native(driver, key, timeout=timeout)
# Tap the field first so Android focuses it.
field.click()
# Clear old text so repeated local runs do not append values.
if clear_first:
field.clear()
# send_keys types through the native Android input.
field.send_keys(value)
שימוש:
type_text(android_apk, "name_input", TEST_USER["name"])
type_text(android_apk, "email_input", TEST_USER["email"])
סגירת מקלדת
במובייל המקלדת יכולה להסתיר כפתורים, radio buttons או checkbox.
לכן אחרי הכנסת טקסט כדאי לסגור אותה בצורה בטוחה.
def hide_keyboard_safely(driver) -> None:
"""Hide the Android keyboard when it is open and ignore it when it is already closed."""
# Native mobile keyboards can cover radio buttons, checkboxes, and submit buttons.
try:
driver.hide_keyboard()
except Exception:
# Some devices raise an error when the keyboard is already hidden.
pass
בחירת radio button
בדוגמה הזאת יש בחירת תוכנית: Starter, Pro, Enterprise.
במקום לכתוב לוקייטור בכל פעם, אני מתרגם ערך עסקי כמו pro ל-Accessibility ID המתאים.
def select_plan(driver, plan: str) -> None:
"""Select one native Android radio button by friendly plan name."""
# Keep business values separate from native accessibility IDs.
plan_key = {
"starter": "plan_starter",
"pro": "plan_pro",
"enterprise": "plan_enterprise",
}[plan.lower()]
# Tap the requested native radio button.
tap(driver, plan_key)
שימוש:
select_plan(android_apk, TEST_USER["plan"])
Checkbox בצורה יציבה
לא כדאי ללחוץ על checkbox בלי לבדוק את המצב שלו.
אם הוא כבר מסומן ולוחצים שוב, הוא יהפוך ללא מסומן.
לכן קודם קוראים את המצב ואז מחליטים אם ללחוץ.
def set_checkbox(driver, key: str, expected_checked: bool) -> None:
"""Set a native Android checkbox to the requested state without double-tapping it."""
# Wait for the checkbox before reading its native checked state.
checkbox = visible_native(driver, key)
# Android exposes checked state as a string attribute.
is_checked = checkbox.get_attribute("checked") == "true"
# Tap only when the current state does not match the desired state.
if is_checked != expected_checked:
checkbox.click()
שימוש:
set_checkbox(android_apk, "terms_checkbox", True)
בדיקת status
בדמו הזה יש label בשם status שמראה אם פעולה הצליחה.
במקום לבדוק אותו ידנית בכל פעם, יש helper אחד.
def assert_status(driver, expected_text: str, timeout: int = 10) -> None:
"""Wait until the native Android status label contains expected text."""
# The status label is the main proof that each native action worked.
wait_for(driver, timeout).until(ec.text_to_be_present_in_element(aid("status"), expected_text))
שימוש:
assert_status(android_apk, "Practice started")
assert_status(android_apk, "Order saved")
Scroll באפליקציה Native
ב-Android Native הדרך הנפוצה לגלול לאלמנט היא להשתמש ב-UiScrollable.
זה לא JavaScript scroll, כי אנחנו לא בדפדפן אלא באפליקציה Native.
def scroll_to_deep_target(driver):
"""Scroll the native Android view until the deep target accessibility ID is visible."""
# UiScrollable is the standard Android-native scroll helper for off-screen elements.
return driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiScrollable(new UiSelector().scrollable(true))'
'.scrollIntoView(new UiSelector().description("android-deep-target"))',
)
שימוש:
deep_target = scroll_to_deep_target(android_apk)
assert deep_target.text == "Deep scroll target"
צירוף מצב לדוח Allure
בסוף הטסט אפשר לצרף לדוח Allure מצב קצר של האפליקציה.
חשוב: attachment לא אמור להפיל את הטסט אם אלמנט כבר לא נראה בגלל scroll.
לכן משתמשים בקריאה בטוחה.
def attach_native_state(driver) -> None:
"""Attach useful Android native app state to Allure for debugging failed flows."""
# Debug attachments should never fail the test if the page is scrolled away from top elements.
def safe_text(key: str) -> str:
try:
# Use a short wait because this is only report enrichment.
return visible_native(driver, key, timeout=2).text
except Exception:
# Return a readable placeholder when the element is off-screen or unavailable.
return "<not visible>"
# Read the app title when it is visible.
title = safe_text("title")
# Read the current status label when it is visible.
status = safe_text("status")
# Attach a compact native state summary to the Allure report.
attach_text("Android APK native state", f"title={title}\nstatus={status}")
דוגמה ל-flow מלא
עכשיו אפשר לראות איך כל החלקים מתחברים לטסט אחד קריא.
def test_android_apk_core_actions(android_apk):
"""Teach a longer Appium native Android flow that can be copied for real APKs."""
# Start an Allure step for initial native app checks.
with allure_step("Open native Android app and verify stable landing state"):
# Verify the native title is visible before starting user actions.
assert visible_native(android_apk, "title").text == "Wulf Demo Android"
# Use the assert_status helper to verify the initial status text.
assert_status(android_apk, "Ready")
# Start an Allure step for the first native tap.
with allure_step("Start practice with a native Android tap"):
# Use the tap helper to wait for the native start button and tap it.
tap(android_apk, "start_button")
# Use the assert_status helper to confirm the tap changed the app state.
assert_status(android_apk, "Practice started")
# Start an Allure step for native input and controls.
with allure_step("Fill native Android fields and submit order"):
# Use the type_text helper to focus, clear, and type the user's display name.
type_text(android_apk, "name_input", TEST_USER["name"])
# Use the type_text helper to focus, clear, and type the user's email.
type_text(android_apk, "email_input", TEST_USER["email"])
# Use the hide_keyboard_safely helper before interacting with lower controls.
hide_keyboard_safely(android_apk)
# Use the select_plan helper to choose the requested native radio option.
select_plan(android_apk, TEST_USER["plan"])
# Use the set_checkbox helper to set the native checkbox safely.
set_checkbox(android_apk, "terms_checkbox", True)
# Use the tap helper to submit the native form.
tap(android_apk, "submit_button")
# Use the assert_status helper to verify the submit result.
assert_status(android_apk, "Order saved")
# Start an Allure step for a negative validation example.
with allure_step("Toggle terms off and verify native validation"):
# Use the set_checkbox helper to turn the terms checkbox off.
set_checkbox(android_apk, "terms_checkbox", False)
# Use the tap helper to submit without accepted terms.
tap(android_apk, "submit_button")
# Verify the app shows validation instead of saving the order.
assert_status(android_apk, "Please accept demo terms")
# Restore the checkbox state so the flow can continue cleanly.
set_checkbox(android_apk, "terms_checkbox", True)
# Start an Allure step for native Android scrolling.
with allure_step("Scroll to deep native Android content and verify target"):
# Use UiScrollable to move to the off-screen deep target.
deep_target = scroll_to_deep_target(android_apk)
# Assert verifies that the expected native text is visible/readable.
assert deep_target.text == "Deep scroll target"
# Start an Allure step for final debug attachments.
with allure_step("Attach final Android APK native state"):
# Attach useful text values for Allure debugging.
attach_native_state(android_apk)
מה בדרך כלל משנים בפרויקט אחר?
- מחליפים את ANDROID_APP_PATH או package/activity של האפליקציה.
- מחליפים את הערכים בתוך TEST_USER.
- מחליפים את ה-Accessibility IDs בתוך AID.
- משנים את שמות ה-allure_step לפי המסע העסקי האמיתי.
- משאירים את פונקציות העזר, כי הן הופכות flow ארוך לקריא וקל לתחזוקה.
פקודת הרצה
לפני ההרצה צריך Android Emulator פתוח, Appium server פעיל, ו-APK מותקן או מוגדר בפרויקט.
appium --base-path /
ואז מריצים את הטסט מתוך תיקיית הפרויקט:
python -m pytest -q tests/android_apk/test_android_apk.py -s
לסיכום: בבדיקת Android APK המבנה הנכון הוא להפריד בין נתוני בדיקה, לוקייטורים Native,
פונקציות עזר ו-flow עסקי. כך אפשר לקחת את אותו template ולהתאים אותו במהירות לאפליקציה אחרת.