对应代码原项目pages/login_page.py143行说明本节所有代码示例均来自一个真实的移动端自动化测试项目业务名称和API路径已做模糊化处理。上一节把 BasePage 基类搭好了里面封装了click()、input_text()、find_element()这些通用操作。现在轮到具体页面了——拿登录页面开刀。LoginPage 的全部代码就 143 行。我拆成几块聊元素定位常量、业务操作方法、验证方法、文本获取。另外实际项目里还有日志集成这块很多教程不提但线上跑脚本没日志你根本不知道哪崩的。先看继承关系from base.base_page import BasePage from utils.logger import Logger logger Logger().get_logger() class LoginPage(BasePage): def __init__(self, driver): super().__init__(driver) logger.info(初始化登录页面)继承 BasePagesuper().__init__(driver)把 driver 传上去顺带把 BasePage 里的self.waitWebDriverWait(driver, 10)、self.screenshot_helper都初始化好。logger是类级别的整个 LoginPage 共享一个日志实例。元素定位常量为什么用元组翻到base_page.py第 86 行find_element的签名是def find_element(self, locator_type: str, locator_value: str, timeout: int 10):两个参数定位方式 定位值。click()和input_text()也是类似的签名。所以 LoginPage 里把定位信息存成元组用*解包传参# login_page.py 第 18-23 行 LOGIN_TITLE (accessibility_id, login_title) PHONE_EMAIL_INPUT (accessibility_id, phone_email_input) PASSWORD_INPUT (acce...id, password_input) # 实际取值需要替换 LOGIN_BUTTON (accessibility_id, login_button) FORGOT_PASSWORD_LINK (acce...id, forgot_password_link) REGISTER_LINK (accessibility_id, register_link)调用时self.click(*self.LOGIN_BUTTON) # 等价于 self.click(accessibility_id, login_button)注意PASSWORD_INPUT和FORGOT_PASSWORD_LINK的定位值写的是acce...id——这是故意的还是失误看注释就知道这些 accessibility_id 是占位符实际必须用 Appium Inspector 抓真实值替换。忘掉这一步直接跑你会看到这种报错selenium.common.exceptions.NoSuchElementException: Message: An element could not be located on the page using the given search parameters.或者更具体一点的selenium.common.exceptions.TimeoutException: Message: Expected condition failed: waiting for presence of element located by: ByAccessibilityId: acce...id (tried for 10 second(s) with 500 milliseconds interval)定位值写acce...id这种假的字符串find_element等 10 秒超时后抛TimeoutException测试直接红。所以项目里注释也写了需要用 Appium Inspector 获取真实 ID。业务操作方法——组合 BasePage 的基础操作base_page.py的input_text()第 209 行和click()第 168 行已经处理了找元素、等待、点击、异常截图这些脏活。LoginPage 要做的就是传正确的定位常量def input_phone_email(self, value: str): logger.info(f输入手机号/邮箱: {value}) self.input_text(*self.PHONE_EMAIL_INPUT, value) time.sleep(0.5) def input_password(self, value: str): logger.info(f输入密码: {value}) self.input_text(*self.PASSWORD_INPUT, value) time.sleep(0.5) def click_login_button(self): logger.info(点击登录按钮) self.click(*self.LOGIN_BUTTON) time.sleep(2) def click_forgot_password_link(self): logger.info(点击忘记密码链接) self.click(*self.FORGOT_PASSWORD_LINK) time.sleep(2) def click_register_link(self): logger.info(点击注册链接) self.click(*self.REGISTER_LINK) time.sleep(2)每个方法后面都跟了time.sleep()。有人觉得这是硬等、不优雅但移动端自动化里这些 sleep 是有道理的输入后sleep(0.5)——键盘弹起/收起需要时间。不等的话下一个操作可能点到键盘而不是页面元素。实际遇到过send_keys成功但后续click抛WebDriverException: Message: An unknown server-side error occurred while processing the command. Original error: POST /element/:id/click cannot be proxied to UiAutomator2 server because the element is not in the active view——就是键盘动画还没结束。点击跳转类操作后sleep(2)——页面转场动画 网络请求。不等就操作下一个页面元素大概率StaleElementReferenceException元素还停留在上一个页面的 DOM 里。验证方法——吞异常返回布尔值验证方法的目标是不抛异常。测试用例不需要用pytest.raises包一层直接用if判断就行。def verify_login_title_exists(self) - bool: try: element self.find_element(*self.LOGIN_TITLE, timeout5) return element is not None and element.is_displayed() except Exception as e: logger.warning(f验证登录标题失败: {str(e)}) return False def verify_phone_email_input_enabled(self) - bool: try: element self.find_element(*self.PHONE_EMAIL_INPUT, timeout5) return element is not None and element.is_enabled() except Exception as e: logger.warning(f验证输入框失败: {str(e)}) return False def verify_password_input_enabled(self) - bool: try: element self.find_element(*self.PASSWORD_INPUT, timeout5) return element is not None and element.is_enabled() except Exception as e: logger.warning(f验证密码输入框失败: {str(e)}) return False def verify_login_button_enabled(self) - bool: try: element self.find_element(*self.LOGIN_BUTTON, timeout5) return element is not None and element.is_enabled() except Exception as e: logger.warning(f验证登录按钮失败: {str(e)}) return Falsetimeout5是个合理的妥协——页面渲染慢时等 5 秒够了再长测试用例跑得慢。element.is_displayed()检查元素有没有被遮挡或不在可视区域is_enabled()检查按钮是否灰掉比如没输完手机号时登录按钮是 disabled 状态。这里有个实际踩过的坑你在 Appium Inspector 里看到元素存在但脚本里find_element就是超时。报错selenium.common.exceptions.TimeoutException: Message: Expected condition failed: waiting for presence of element located by: ByAccessibilityId: login_button (tried for 5 second(s) with 500 milliseconds interval)可能的原因元素在 native 上下文里但脚本切到了 webview 上下文或者反过来。用driver.contexts打印一下当前可用上下文确认。另一个坑is_displayed()在 Appium 1.x 某些版本里对某些 Android 原生控件会抛异常selenium.common.exceptions.WebDriverException: Message: Unknown error: getText is not a function这时候直接return element is not None就够用跳过is_displayed()。文本获取方法def get_phone_email_input_text(self) - str: try: element self.find_element(*self.PHONE_EMAIL_INPUT, timeout5) return element.text if element else except Exception as e: logger.warning(f获取输入框文本失败: {str(e)}) return 适用场景挺直接的输入后回读验证——比如输入 testexample.com 后get_phone_email_input_text()确认内容一致获取错误提示信息——登录失败后页面上出现 密码不正确 之类文案用文本获取抓回来断言element.text返回的是元素上显示的文本。如果元素不在屏幕上或者页面还没渲染完find_element会抛异常catch 住返回空字符串。测试用例里就可以写error_text login_page.get_phone_email_input_text() assert 密码不正确 in error_text几个容易翻车的地方元素定位常量别搁方法里面定义。有人喜欢在input_phone_email方法里写phone_input (accessibility_id, phone_email_input)每个方法都写一遍维护时改个定位值要改 N 处。写在类级别一处改到处生效。验证方法里 try/except 的范围。有些人只 catchTimeoutException结果element.is_displayed()抛了个AttributeError或者StaleElementReferenceException测试崩了。LoginPage 里直接except Exception所有异常一网打尽返回False。验证方法稳如狗测试用例不用操心异常处理。time.sleep 时长别瞎拍。输入后 0.5 秒够用了点击跳转后 2 秒起步。设太短比如 0.5 秒页面还没跳完下一个find_element直接超时。设太长比如 10 秒一个测试跑下来光 sleep 就占半分钟。2 秒是个经验值如果页面特别慢可以提到 3 秒但应该优先排查为什么慢。一个页面类就管一个屏幕。登录页是一个 screen注册页是另一个 screen分开写。不要把登录按钮和注册页面里的国家选择器塞进同一个类。见过有人把所有页面操作写进一个 800 行的AppPage类后面改一个元素定位要找半天。不在配套代码里的verify_password_input_enabled()——实际项目里确实会用到这个来验证密码框是否可编辑但原配套代码只有 143 行没把所有排列组合都塞进去。你项目里需要什么验证方法就加什么别硬套模板。