בדיקת iPhone App בפרויקט אוטומציה עם Python ו-Appium
מסיבות של אבטחת מידע אני לא מציג כאן פרויקט אמיתי מלא, אבל כן אפשר להציג מבנה עבודה נכון.
בדוגמה הזאת מדובר בבדיקת iPhone App: כלומר בדיקה של אפליקציה Native שרצה על iPhone Simulator או מכשיר iPhone אמיתי.
חשוב להבין: זו לא בדיקת Web בתוך Safari. כאן אנחנו בודקים אפליקציה iOS אמיתית,
ולכן הלוקייטורים הם בדרך כלל Accessibility ID.
ב-iOS זה בדרך כלל מגיע מ-accessibilityIdentifier שהוגדר באפליקציה.
הפונקציה הראשית של הטסט
ב-Pytest כל פונקציה שמתחילה ב-test_ נחשבת לטסט.
במקרה הזה הפונקציה הראשית היא:
def test_iphone_app_core_actions(iphone_app):
"""Teach a longer Appium native iOS flow that can be copied for real iPhone apps."""
לפני שהקוד בתוך הפונקציה רץ, Pytest מכין את ה-fixture בשם:
iphone_app
ה-fixture הזה יוצר Appium driver, פותח את האפליקציה, ואז הטסט מתחיל לעבוד מול המסך של האפליקציה.
הבדל חשוב בין iPhone Safari לבין iPhone App
ב-iPhone Safari אנחנו בודקים אתר, ולכן הלוקייטורים נראים בדרך כלל כך:
(by.CSS_SELECTOR, "[data-testid='email-input']")
אבל ב-iPhone App אנחנו בודקים אפליקציה Native, ולכן הלוקייטור נראה כך:
(AppiumBy.ACCESSIBILITY_ID, "ios-email-input")
כלומר:
- iPhone Safari – אתר Web, משתמשים ב-CSS.
- iPhone App – אפליקציה Native, משתמשים ב-Accessibility ID.
ייבוא הדברים שצריך
בתחילת הקובץ אני מייבא את הכלים הקבועים: Allure לדוחות, Pytest להרצת בדיקות,
AppiumBy ללוקייטורים Native, ו-WebDriverWait להמתנות יציבות.
# Import time so the visual demo scroll is easy to see in the Simulator.
import time
# Import Allure decorators for report grouping.
import allure
# Import pytest to mark this test with environment labels.
import pytest
# Import AppiumBy for native iOS locators.
from appium.webdriver.common.appiumby import AppiumBy
# Import TimeoutException for keyboard fallback handling.
from selenium.common.exceptions import TimeoutException
# 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 iPhone test data near the top so this file is easy to copy.
TEST_USER = {
"name": "iPhone APP",
"email": "[email protected]",
"plan": "Pro",
}
לוקייטורים Native במקום אחד
גם את הלוקייטורים אני שומר במקום אחד. במקרה של Native iOS אני משתמש ב-Accessibility IDs.
כך אם באפליקציה משנים identifier, אני לא צריך לחפש אותו בכל הקובץ.
# Keep native iOS accessibility IDs in one place so locator changes are easy to make.
AID = {
"title": "ios-title",
"status": "ios-status",
"start_button": "ios-start-button",
"name_input": "ios-name-input",
"email_input": "ios-email-input",
"keyboard_done": "ios-keyboard-done",
"plan_picker": "ios-plan-picker",
"terms_toggle": "ios-terms-toggle",
"submit_button": "ios-submit-button",
"deep_target": "ios-deep-target",
}
פונקציית aid
במקום לכתוב בכל פעולה את אותו מבנה של Appium locator, אני משתמש בפונקציה קטנה שמחזירה tuple.
כלומר זוג ערכים: איך לחפש, ומה לחפש.
def aid(key: str) -> tuple[str, str]:
"""Return a native iOS accessibility locator from the ID dictionary."""
# Native iOS tests should prefer accessibility IDs when the app exposes them.
return (AppiumBy.ACCESSIBILITY_ID, AID[key])
לדוגמה:
aid("email_input")
מחזיר בפועל:
(AppiumBy.ACCESSIBILITY_ID, "ios-email-input")
המתנות יציבות
באוטומציה לא כדאי ללחוץ מיד על אלמנט. צריך לחכות שהוא באמת מופיע או לחיץ.
לכן יש helper שמחזיר WebDriverWait:
def wait_for(driver, timeout: int = 10) -> WebDriverWait:
"""Create a WebDriverWait object for the current native iOS 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 iOS 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 iOS element is clickable by accessibility ID."""
# Clickability waits avoid tapping before iOS 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 iOS 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(iphone_app, "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 iOS 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 iOS 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 iOS input.
field.send_keys(value)
שימוש:
type_text(iphone_app, "name_input", TEST_USER["name"])
type_text(iphone_app, "email_input", TEST_USER["email"])
סגירת מקלדת ב-iOS
ב-iPhone המקלדת יכולה להסתיר כפתורים, toggle או שדות אחרים.
בדמו הזה האפליקציה חושפת כפתור Done מעל המקלדת,
ואם הוא לא זמין, יש fallback ל-driver.hide_keyboard().
def hide_keyboard_safely(driver) -> None:
"""Hide the iOS keyboard using the app toolbar button or Appium fallback."""
# The demo iOS app exposes a Done button above the keyboard for reliable automation.
try:
clickable_native(driver, "keyboard_done", timeout=3).click()
except TimeoutException:
# Fall back to Appium keyboard hiding when the toolbar is not exposed.
try:
driver.hide_keyboard()
except Exception:
# Some iOS sessions raise an error when the keyboard is already hidden.
pass
הוצאת focus משדה טקסט
לפעמים אחרי הכנסת טקסט, השדה עדיין בפוקוס והפעולה הבאה פחות יציבה.
לכן אפשר ללחוץ על כותרת המסך כדי להוציא focus מהשדה.
def leave_input_focus(driver) -> None:
"""Tap the title so focus leaves the active input field."""
# This keeps the next native tap from being swallowed by the keyboard or focused input.
visible_native(driver, "title").click()
בחירת plan באפליקציה
בדמו יש segmented picker עם תוכניות כמו Starter, Pro ו-Enterprise.
אם Appium חושף את הטקסט של הסגמנט, אפשר ללחוץ עליו.
אם לא, לפחות מוודאים שה-picker קיים כדי לא להפיל את הדמו.
def select_plan(driver, plan: str) -> None:
"""Select a segmented iOS picker value when Appium exposes the segment text."""
# The demo picker is segmented, so the visible segment text is usually tappable.
try:
driver.find_element(AppiumBy.ACCESSIBILITY_ID, plan).click()
except Exception:
# Keep the flow stable if the current simulator exposes only the picker container.
visible_native(driver, "plan_picker")
Toggle / Switch בצורה יציבה
לא כדאי ללחוץ על toggle בלי לבדוק את המצב שלו.
ב-iOS switch בדרך כלל מחזיר ערך "1" כשהוא דולק ו-"0" כשהוא כבוי.
def set_switch(driver, key: str, expected_on: bool) -> None:
"""Set a native iOS switch/toggle to the requested state without double-tapping it."""
# Wait for the switch before reading its native value.
switch = visible_native(driver, key)
# iOS switches usually expose "1" when on and "0" when off.
is_on = switch.get_attribute("value") == "1"
# Tap only when the current state does not match the desired state.
if is_on != expected_on:
switch.click()
שימוש:
set_switch(iphone_app, "terms_toggle", True)
בדיקת status
בדמו הזה יש label בשם status שמראה אם פעולה הצליחה.
במקום לבדוק אותו ידנית בכל פעם, יש helper אחד.
def assert_status(driver, expected_text: str, timeout: int = 10) -> None:
"""Wait until the native iOS 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))
Scroll באפליקציה Native iOS
ב-iOS Native לא משתמשים ב-JavaScript scroll, כי זו לא בדיקת Web.
במקום זה משתמשים במחוות Native כמו mobile: swipe.
def swipe_up(driver, pause_seconds: float = 0.35) -> None:
"""Perform one native iOS upward swipe and pause briefly for animation."""
# mobile: swipe uses native iOS gestures rather than JavaScript scrolling.
driver.execute_script("mobile: swipe", {"direction": "up"})
# A short pause makes the scroll visually understandable in the Simulator.
time.sleep(pause_seconds)
def scroll_to_deep_target(driver, max_swipes: int = 10):
"""Swipe until the native iOS deep target is visibly inside the screen."""
# Read the screen size so we can confirm the element rectangle is actually visible.
screen = driver.get_window_size()
# Swipe a bounded number of times so a broken locator cannot create an infinite loop.
for _ in range(max_swipes):
# iOS can sometimes find off-screen accessibility elements, so check its rectangle.
candidate = driver.find_element(AppiumBy.ACCESSIBILITY_ID, AID["deep_target"])
rect = candidate.rect
# Stop when the deep target is inside the visible screen.
if 0 <= rect["y"] <= screen["height"] - rect["height"]:
return candidate
# Swipe upward to move lower content into view.
swipe_up(driver)
# Fall back to a normal visibility wait if the bounded swipe loop did not stop.
return visible_native(driver, "deep_target")
צירוף מצב לדוח Allure
בסוף הטסט אפשר לצרף לדוח Allure מצב קצר של האפליקציה.
חשוב: attachment לא אמור להפיל את הטסט אם אלמנט כבר לא נראה בגלל scroll.
לכן משתמשים בקריאה בטוחה.
def attach_native_state(driver) -> None:
"""Attach useful iPhone 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("iPhone APP native state", f"title={title}\nstatus={status}")
דוגמה ל-flow מלא
עכשיו אפשר לראות איך כל החלקים מתחברים לטסט אחד קריא.
def test_iphone_app_core_actions(iphone_app):
"""Teach a longer Appium native iOS flow that can be copied for real iPhone apps."""
# Start an Allure step for initial native app checks.
with allure_step("Open native iPhone app and verify stable landing state"):
# Verify the native title is visible before starting user actions.
assert visible_native(iphone_app, "title").text == "Wulf Demo iOS"
# Use the assert_status helper to verify the initial status text.
assert_status(iphone_app, "Ready")
# Start an Allure step for the first native tap.
with allure_step("Start practice with a native iOS tap"):
# Use the tap helper to wait for the native start button and tap it.
tap(iphone_app, "start_button")
# Use the assert_status helper to confirm the tap changed the app state.
assert_status(iphone_app, "Practice started")
# Start an Allure step for native input and controls.
with allure_step("Fill native iPhone fields and submit order"):
# Use the type_text helper to focus, clear, and type the user's display name.
type_text(iphone_app, "name_input", TEST_USER["name"])
# Use the type_text helper to focus, clear, and type the user's email.
type_text(iphone_app, "email_input", TEST_USER["email"])
# Use the hide_keyboard_safely helper before interacting with lower controls.
hide_keyboard_safely(iphone_app)
# Use the leave_input_focus helper to make the next native tap stable.
leave_input_focus(iphone_app)
# Use the select_plan helper for the segmented native picker.
select_plan(iphone_app, TEST_USER["plan"])
# Use the set_switch helper to set the native toggle safely.
set_switch(iphone_app, "terms_toggle", True)
# Use the tap helper to submit the native form.
tap(iphone_app, "submit_button")
# Use the assert_status helper to verify the submit result.
assert_status(iphone_app, "Order saved")
# Start an Allure step for a negative validation example.
with allure_step("Toggle terms off and verify native iOS validation"):
# Use the set_switch helper to turn terms off.
set_switch(iphone_app, "terms_toggle", False)
# Use the tap helper to submit without accepted terms.
tap(iphone_app, "submit_button")
# Verify the app shows validation instead of saving the order.
assert_status(iphone_app, "Please accept demo terms")
# Restore the switch state so the flow can continue cleanly.
set_switch(iphone_app, "terms_toggle", True)
# Start an Allure step for native iOS scrolling.
with allure_step("Scroll to deep native iPhone content and verify target"):
# Use bounded native swipes to move to the off-screen deep target.
deep_target = scroll_to_deep_target(iphone_app)
# 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 iPhone APP native state"):
# Attach useful text values for Allure debugging.
attach_native_state(iphone_app)
מה בדרך כלל משנים בפרויקט אחר?
- מחליפים את IOS_APP_PATH או bundle id של האפליקציה.
- מחליפים את הערכים בתוך TEST_USER.
- מחליפים את ה-Accessibility IDs בתוך AID.
- משנים את שמות ה-allure_step לפי המסע העסקי האמיתי.
- משאירים את פונקציות העזר, כי הן הופכות flow ארוך לקריא וקל לתחזוקה.
פקודת הרצה
לפני ההרצה צריך iPhone Simulator פתוח, Appium server פעיל, ואפליקציה בנויה או מותקנת.
appium --base-path /
ואז מריצים את הטסט מתוך תיקיית הפרויקט:
python -m pytest -q tests/iphone_app/test_iphone_app.py -s
לסיכום: בבדיקת iPhone App המבנה הנכון הוא להפריד בין נתוני בדיקה, לוקייטורים Native,
פונקציות עזר ו-flow עסקי. כך אפשר לקחת את אותו template ולהתאים אותו במהירות לאפליקציית iOS אחרת.