1. 项目概述:为什么需要扩展Appium Python Client?

如果你已经用Appium Python Client写过一段时间的自动化测试脚本,大概率会遇到过这样的场景:官方库提供的 click send_keys find_element 这些标准命令用起来很顺手,但一到某些特定需求,比如想直接调用一个WebDriver协议里支持但Client库没封装的底层命令,或者想精细化管理设备连接的生命周期,就会感觉有点“使不上劲”。官方Client库为了保持通用性和稳定性,通常只封装最常用、最稳定的那部分功能。而实际项目中,尤其是面对碎片化严重的安卓设备群、需要与内部测试平台对接,或者有特殊性能监控需求时,原生的Client就显得不够灵活了。

这就是我们今天要聊的核心: 扩展Appium Python Client 。这不是简单地调用几个API,而是深入到Client库的架构层面,去定制和增强它的能力。具体来说,主要围绕两个方向:一是 自定义命令(Custom Commands) ,让你能像调用 driver.find_element 一样,轻松执行任何符合WebDriver协议或甚至是你自己服务端定义的指令;二是 连接管理(Connection Management) ,这关乎测试脚本的健壮性和执行效率,如何优雅地处理连接建立、重试、复用和销毁,尤其是在多设备并行或长时间运行的稳定性测试中。

我经历过不少因为连接超时导致测试用例莫名失败,或者因为某个特殊操作没有现成方法而不得不写一堆底层HTTP请求代码的情况。后来发现,与其每次“打补丁”,不如系统地掌握扩展Client的方法。这不仅能提升脚本的复用性和可读性,更能让你对Appium的运作机制有更深的理解,从“使用者”转变为“定制者”。接下来,我会结合代码,把自定义命令和连接管理这两块硬骨头拆开、揉碎,讲清楚里面的门道和实操中容易踩的坑。

2. 理解Appium Python Client的架构与扩展点

在动手写代码之前,我们必须先搞清楚Appium Python Client是怎么工作的。它不是一个黑盒子,而是一个清晰的分层结构。知其然,更要知其所以然,这样我们扩展的时候才能找到正确的“穴位”。

2.1 核心架构:从WebDriver到你的脚本

Appium Python Client(以下简称 appium-python-client )本质上是Selenium Python Client的一个扩展。它的核心是 webdriver.Remote 类。当你执行 driver = webdriver.Remote(command_executor, desired_capabilities) 时,发生的事情是这样的:

  1. 命令翻译层 :你调用的所有方法,如 driver.find_element(By.ID, “button”) ,都会被 Remote 对象转换成一个标准的JSON Wire Protocol或W3C WebDriver协议请求。
  2. HTTP通信层 :这个请求通过HTTP POST发送到你指定的 command_executor (通常是Appium Server的地址,如 http://localhost:4723 )。
  3. 服务端处理 :Appium Server接收到请求,解析命令,并通过对应的设备驱动(如UiAutomator2 for Android, XCUITest for iOS)在真实设备或模拟器上执行操作。
  4. 响应返回 :操作结果被封装成HTTP响应返回给Client,Client再解析响应,可能返回一个WebElement对象,也可能只是返回一个状态。

appium-python-client 在这个链条中做了什么?它做了两件关键事:一是 扩展了Desired Capabilities ,增加了 appium:appPackage appium:automationName 等Appium特有的配置项;二是 混入(Mixin)了一系列Helper类 ,比如 AppiumBy FindsByImage 等,为 Remote 对象添加了 find_element_by_image start_activity 等移动端特有方法。

2.2 关键扩展点:Command和Connection

我们的扩展工作,主要针对两个内部机制:

  • Command(命令)机制 :这是 webdriver.Remote 内部维护的一个命令字典( _commands 属性)。它定义了方法名(如 FIND_ELEMENT )到具体远程请求路径(如 /session/:session_id/element )和HTTP方法(POST)的映射。当我们想添加一个官方库没有的命令时,就需要操作这个机制。
  • RemoteConnection(远程连接)类 :这是真正负责发送HTTP请求、处理响应的类。它管理着连接超时、请求重试、错误处理等底层网络细节。我们要增强连接稳定性、添加日志或重试逻辑,就需要从这里入手。

理解这两点,就像拿到了扩展Client的“地图”。自定义命令让我们可以绘制新的“目的地”(命令),而自定义连接管理则让我们可以优化“交通工具”(网络连接)的性能和可靠性。

3. 实战一:开发自定义命令(Custom Commands)

自定义命令是扩展Client最直接、最常用的方式。它允许你将任何HTTP端点封装成一个直观的Python方法。

3.1 自定义命令的原理与步骤

原理很简单:在 webdriver.Remote 实例的 _commands 字典中注册一个新的映射,然后定义一个对应的方法来发起请求。

假设我们需要一个官方库未提供的命令: 获取当前设备的屏幕分辨率 。虽然可以通过 driver.get_window_size() 获取,但假设Appium Server提供了一个更直接的内部端点 /session/:session_id/appium/device/screen_info

步骤拆解:

  1. 定义命令常量 :给它起个唯一的名字,比如 GET_SCREEN_INFO
  2. 注册命令映射 :告诉Client,这个命令常量对应哪个HTTP路径和方法。
  3. 实现命令方法 :在自定义的WebDriver类中添加一个方法,该方法内部会调用注册的命令。

3.2 完整代码示例与逐行解析

下面我们创建一个自定义的WebDriver类。

# custom_driver.py
from appium.webdriver.webdriver import WebDriver as AppiumWebDriver
from selenium.webdriver.remote.command import Command as SeleniumCommand

# 1. 定义自定义命令常量。为了避免冲突,建议使用独特的前缀。
class CustomCommand:
    GET_SCREEN_INFO = ("GET", "/session/:session_id/appium/device/screen_info")

class CustomAppiumDriver(AppiumWebDriver):
    def __init__(self, *args, **kwargs):
        # 首先调用父类的初始化方法,建立标准连接和命令集
        super().__init__(*args, **kwargs)
        # 2. 在初始化时,将自定义命令注册到驱动程序的命令执行器中。
        #    `self._commands` 是一个字典,键是命令名,值是 (method, url) 元组。
        self._commands[CustomCommand.GET_SCREEN_INFO] = CustomCommand.GET_SCREEN_INFO

    def get_screen_info(self):
        """
        自定义方法:获取设备屏幕的详细信息(分辨率、密度等)。
        返回一个包含屏幕信息的字典。
        """
        # 3. 使用 `self.execute` 方法执行自定义命令。
        #    `execute` 方法会查找 `self._commands` 中的映射,构造并发送HTTP请求。
        #    第一个参数是我们注册的命令常量。
        screen_info = self.execute(CustomCommand.GET_SCREEN_INFO, {})
        # 通常,响应体中的 `value` 字段包含了服务端返回的主要数据。
        return screen_info.get('value', {}) if isinstance(screen_info, dict) else screen_info

    # 再举一个例子:一个需要传递参数的复杂命令,比如设置设备网络连接状态。
    # 假设端点:POST /session/:session_id/appium/device/network_connection
    SET_NETWORK_CONNECTION = ("POST", "/session/:session_id/appium/device/network_connection")
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._commands[CustomCommand.GET_SCREEN_INFO] = CustomCommand.GET_SCREEN_INFO
        # 注册第二个命令
        self._commands['SET_NETWORK_CONNECTION'] = self.SET_NETWORK_CONNECTION

    def set_network_connection(self, connection_type):
        """
        设置设备网络连接类型。
        :param connection_type: 整数,代表网络类型(如 0: 无网络, 1: 飞行模式, 2: WIFI only, 4: 数据 only, 6: 所有网络)
        """
        # 构建请求体参数
        params = {'type': connection_type}
        # 执行命令,第二个参数是请求体数据
        result = self.execute('SET_NETWORK_CONNECTION', params)
        return result

代码解析与注意事项:

  • 命令常量格式 (HTTP_METHOD, “URL_PATH”) 。URL中的 :session_id 是占位符, execute 方法会自动用当前会话的真实ID替换它。这是Selenium/Appium Client内部的标准约定。
  • 注册时机 :必须在 __init__ 中,调用 super().__init__() 之后 进行注册。因为父类的初始化会建立基础的命令字典,我们是在此基础上做扩展。
  • execute 方法 :这是核心。它接收命令名和参数字典。参数字典会被序列化为JSON,作为请求体(对于POST/PUT)或查询参数(对于GET/DELETE,但Appium命令多为POST)发送。
  • 错误处理 execute 方法内部已经包含了基本的HTTP错误和WebDriver错误处理。但如果你的服务端端点返回非标准格式,可能需要在这个自定义方法里额外处理响应。

注意 :在注册命令时,键(如 ‘SET_NETWORK_CONNECTION’ )只是一个内部标识符,理论上可以任意字符串,但为了清晰,通常与常量或方法名保持一致。而值必须是 (method, url) 元组。

3.3 使用自定义Driver

使用起来和原生Driver完全一样:

from appium import webdriver
from custom_driver import CustomAppiumDriver # 导入我们自定义的类

desired_caps = {
    'platformName': 'Android',
    'appium:deviceName': 'emulator-5554',
    'appium:appPackage': 'com.example.app',
    'appium:appActivity': '.MainActivity'
}

# 关键:使用 custom_driver 模块中的 CustomAppiumDriver
driver = CustomAppiumDriver('http://localhost:4723', desired_caps)

try:
    # 使用原生方法
    element = driver.find_element(by=AppiumBy.ID, value='login_button')
    element.click()
    
    # 使用我们自定义的方法!
    screen_info = driver.get_screen_info()
    print(f"屏幕分辨率: {screen_info.get('width')}x{screen_info.get('height')}")
    
    # 设置网络为仅WIFI
    driver.set_network_connection(2)
    
finally:
    driver.quit()

通过这种方式,你将业务相关的、重复的底层HTTP调用封装成了语义清晰的方法,大大提升了代码的可维护性。

4. 实战二:实现自定义连接管理(Connection Management)

连接管理关乎测试的稳定性和资源效率。默认的 RemoteConnection 在简单场景下够用,但在复杂网络环境或追求高稳定性的自动化流水线中,往往需要定制。

4.1 为什么需要自定义连接管理?

默认连接可能存在的痛点:

  1. 超时策略僵化 :默认的读写超时可能不适用于所有操作。例如,安装一个大型APK需要更长的超时时间。
  2. 无自动重试 :一次网络抖动或Appium Server的短暂GC停顿就可能导致命令失败,测试用例被误判。
  3. 日志信息不足 :出问题时,只有简单的错误信息,难以定位是网络问题、服务端问题还是脚本问题。
  4. 连接无法复用 :对于需要频繁创建销毁Session的测试套件,每次都建立全新的HTTP连接会有开销。

4.2 创建自定义RemoteConnection类

我们将创建一个自定义的连接类,主要增加 重试机制 更详细的日志

# custom_connection.py
import logging
import time
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.remote.remote_connection import RemoteConnection
import json

class RetryableRemoteConnection(RemoteConnection):
    """
    自定义远程连接类,支持失败重试和增强日志。
    """
    def __init__(self, remote_server_addr, keep_alive=True, retry_count=3, retry_delay=1, timeout=30):
        """
        初始化自定义连接。
        :param remote_server_addr: Appium Server地址
        :param keep_alive: 是否保持HTTP长连接
        :param retry_count: 命令失败后的重试次数
        :param retry_delay: 重试之间的延迟(秒)
        :param timeout: 默认超时时间(秒)
        """
        # 调用父类初始化,注意父类可能需要`timeout`参数
        super().__init__(remote_server_addr, keep_alive=keep_alive)
        self.retry_count = retry_count
        self.retry_delay = retry_delay
        self._timeout = timeout
        # 设置一个专门的logger
        self.logger = logging.getLogger(__name__)
        
    def execute(self, command, params=None):
        """
        重写execute方法,加入重试逻辑。
        :param command: 命令名
        :param params: 参数字典
        :return: 远程服务器返回的JSON响应
        """
        params = params or {}
        last_exception = None
        
        # 重试循环
        for attempt in range(self.retry_count + 1): # +1 包括第一次尝试
            try:
                self.logger.debug(f"尝试执行命令 [{command}], 参数: {json.dumps(params)[:200]}... (尝试 {attempt + 1}/{self.retry_count + 1})")
                # 调用父类的execute方法执行实际的HTTP请求
                response = super().execute(command, params)
                self.logger.debug(f"命令 [{command}] 执行成功。")
                return response
            except (WebDriverException, IOError) as e:
                last_exception = e
                self.logger.warning(f"命令 [{command}] 第{attempt + 1}次尝试失败: {str(e)[:100]}")
                if attempt < self.retry_count: # 如果不是最后一次尝试,则等待后重试
                    time.sleep(self.retry_delay)
                else:
                    self.logger.error(f"命令 [{command}] 在{self.retry_count + 1}次尝试后均失败。")
                    raise # 重试耗尽,抛出最后的异常
        # 理论上不会执行到这里,因为上面循环内会raise
        raise last_exception

    # 可选:重写_request方法以添加更底层的日志(如HTTP状态码、响应头)
    # def _request(self, *args, **kwargs):
    #     self.logger.debug(f"发起HTTP请求: {args}")
    #     response = super()._request(*args, **kwargs)
    #     self.logger.debug(f"收到HTTP响应,状态码: {response.status}")
    #     return response

4.3 将自定义连接注入到Driver中

仅仅定义了 RetryableRemoteConnection 还不够,我们需要让 CustomAppiumDriver 使用它。这需要重写Driver的 create_connection 类方法。

# 在 custom_driver.py 中更新 CustomAppiumDriver 类
from custom_connection import RetryableRemoteConnection

class CustomAppiumDriver(AppiumWebDriver):
    def __init__(self, *args, **kwargs):
        # 可以从kwargs中提取自定义的连接参数,如重试次数
        self.retry_count = kwargs.pop('retry_count', 3)
        self.retry_delay = kwargs.pop('retry_delay', 1)
        super().__init__(*args, **kwargs)
        self._commands[CustomCommand.GET_SCREEN_INFO] = CustomCommand.GET_SCREEN_INFO

    @classmethod
    def create_connection(cls, keep_alive=True, timeout=30):
        """
        重写此方法,返回我们自定义的连接类实例。
        这个方法会被父类的 __init__ 调用。
        """
        # 注意:这里无法直接获取到实例的 retry_count 参数,因为这是类方法。
        # 一种方案是通过类属性或全局配置传递,另一种更灵活的方式是在 __init__ 中替换连接对象。
        # 这里演示第二种更直接的方式:在 __init__ 中替换。
        pass # 我们先保留空实现,实际工作在 __init__ 中做。

    def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub',
                 desired_capabilities=None, browser_profile=None, proxy=None,
                 keep_alive=True, direct_connection=False, retry_count=3, retry_delay=1, **kwargs):
        """
        扩展初始化方法,支持重试参数,并替换连接对象。
        """
        # 1. 保存重试参数到实例变量
        self._retry_count = retry_count
        self._retry_delay = retry_delay
        
        # 2. 调用父类初始化(此时会使用默认的RemoteConnection)
        super().__init__(command_executor, desired_capabilities, browser_profile,
                         proxy, keep_alive, direct_connection, **kwargs)
        
        # 3. 初始化完成后,替换掉self._command_executor内部的连接对象
        #    `self._command_executor` 是一个 `RemoteConnection` 实例。
        #    我们创建一个新的自定义连接实例来替换它。
        custom_conn = RetryableRemoteConnection(
            remote_server_addr=command_executor,
            keep_alive=keep_alive,
            retry_count=self._retry_count,
            retry_delay=self._retry_delay,
            timeout=self._command_executor._timeout # 继承原有的超时设置
        )
        self._command_executor = custom_conn
        
        # 4. 注册自定义命令
        self._commands[CustomCommand.GET_SCREEN_INFO] = CustomCommand.GET_SCREEN_INFO

关键点解析:

  • 参数传递 :我们将 retry_count retry_delay 作为Driver初始化参数传入,并在内部传递给 RetryableRemoteConnection 。这样使用起来非常直观。
  • 替换时机 :在父类 __init__ 执行完毕后替换 _command_executor 。因为父类初始化过程中已经创建了一个默认的 RemoteConnection 实例,我们需要用增强版覆盖它。
  • 保持兼容性 :我们复制了原有连接的 keep_alive timeout 设置,确保行为一致。

4.4 使用增强版的Driver

现在,你可以创建一个具备自动重试能力的Driver了:

from custom_driver import CustomAppiumDriver

desired_caps = {...} # 你的能力配置

# 创建Driver时指定重试参数
driver = CustomAppiumDriver(
    command_executor='http://localhost:4723',
    desired_capabilities=desired_caps,
    retry_count=2,       # 失败后重试2次
    retry_delay=1.5,     # 每次重试间隔1.5秒
    keep_alive=True
)

try:
    # 现在,所有通过这个driver发出的命令(包括find_element, click等)都自带重试机制!
    el = driver.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='Submit')
    el.click() # 如果点击因网络问题失败,会自动重试最多2次
    info = driver.get_screen_info() # 自定义命令也享受重试机制
    print(info)
finally:
    driver.quit()

5. 高级技巧与集成实践

掌握了基本扩展方法后,我们来看看如何将这些技巧应用到更实际的复杂场景中。

5.1 封装常用操作组合为高阶命令

自定义命令不限于单个HTTP端点。你可以封装一系列操作,形成一个高阶的“业务命令”。

例如,一个常见的场景是 等待某个元素出现并点击,如果失败则截图 。我们可以把它封装起来:

class CustomAppiumDriver(AppiumWebDriver):
    ... # 之前的代码

    def wait_and_click(self, by, selector, timeout=10, screenshot_on_fail=True):
        """
        等待元素出现并点击,失败时可选截图。
        :param by: 定位策略 (AppiumBy.ID, AppiumBy.XPATH等)
        :param selector: 定位器
        :param timeout: 等待超时时间(秒)
        :param screenshot_on_fail: 失败时是否截图
        :return: 被点击的WebElement对象
        """
        from selenium.webdriver.support.ui import WebDriverWait
        from selenium.webdriver.support import expected_conditions as EC
        from selenium.common.exceptions import TimeoutException
        
        try:
            self.logger.info(f"等待元素 [{by}: {selector}] 出现,超时 {timeout}秒")
            element = WebDriverWait(self, timeout).until(
                EC.presence_of_element_located((by, selector))
            )
            self.logger.info(f"元素找到,准备点击。")
            element.click()
            return element
        except TimeoutException:
            self.logger.error(f"等待元素 [{by}: {selector}] 超时。")
            if screenshot_on_fail:
                screenshot_path = f"screenshot_fail_{int(time.time())}.png"
                self.save_screenshot(screenshot_path)
                self.logger.error(f"已保存失败截图至: {screenshot_path}")
            raise # 重新抛出异常,让调用者处理

5.2 与Pytest/Unittest测试框架深度集成

在自动化测试框架中,我们通常希望Driver的创建和销毁由框架管理(如 setup / teardown )。我们可以创建一个 Driver Fixture(Pytest) setUpClass方法(Unittest) ,并在这里注入我们的自定义Driver。

Pytest示例:

# conftest.py
import pytest
from custom_driver import CustomAppiumDriver

@pytest.fixture(scope="function") # 每个测试函数一个driver
def appium_driver(request):
    """
    提供一个配置好的自定义Appium Driver fixture。
    """
    desired_caps = {
        'platformName': 'Android',
        'appium:platformVersion': '11',
        'appium:deviceName': 'Android Emulator',
        'appium:app': '/path/to/your/app.apk',
        'appium:automationName': 'UiAutomator2',
        'appium:noReset': False
    }
    
    # 使用自定义Driver,并传入重试参数
    driver = CustomAppiumDriver(
        command_executor='http://localhost:4723',
        desired_capabilities=desired_caps,
        retry_count=2,
        retry_delay=1
    )
    
    yield driver # 将driver提供给测试用例
    
    # 测试结束后,无论成功失败,都退出driver
    driver.quit()

# 在测试用例中直接使用
def test_login(appium_driver): # pytest会自动注入fixture
    driver = appium_driver
    # 使用自定义方法
    driver.wait_and_click(AppiumBy.ID, 'com.example:id/username_field')
    driver.find_element(AppiumBy.ID, 'com.example:id/username_field').send_keys('testuser')
    # ...
    screen_info = driver.get_screen_info()
    assert screen_info['width'] == 1080

5.3 性能考量与连接池化(高级)

对于大规模并行测试,频繁创建销毁HTTP连接( RemoteConnection )会有开销。虽然HTTP/1.1的 keep_alive 已经帮我们复用了TCP连接,但 RemoteConnection 对象本身以及其内部的 HTTPConnection 对象仍然可能被频繁创建。

一个更高级的优化是 实现一个简单的连接池 。但请注意,由于 RemoteConnection 与Driver Session紧密绑定(尤其是包含 session_id ),通常 不建议池化 RemoteConnection 实例本身 。更可行的优化是池化 Driver实例 ,特别是对于需要快速执行大量独立测试套件的场景。这涉及到更复杂的生命周期管理,通常需要与测试框架和任务调度器(如pytest-xdist, Celery)结合,超出了本篇基础教程的范围。一个简单的起点是,在 conftest.py 的fixture中使用 scope="session" ,让所有测试用例共享同一个Driver实例(需确保测试用例之间不会相互干扰)。

6. 常见问题排查与调试技巧

扩展开发过程中,难免会遇到问题。这里记录几个我踩过的坑和解决方法。

6.1 自定义命令执行失败:404或405错误

  • 问题 :调用自定义命令时,Appium Server返回 404 Not Found 405 Method Not Allowed
  • 排查
    1. 检查URL和Method :首先确认你注册的命令元组 (METHOD, URL) 是否正确。与Appium Server官方文档或源码中的端点定义仔细比对。注意URL路径是相对于Server根路径的。
    2. 检查Session ID :确保URL中的 :session_id 占位符格式正确。Client会自动替换它。如果你手动拼接URL,可能会出错。
    3. 使用抓包工具 :用 mitmproxy Charles Fiddler 抓包,查看实际发出的HTTP请求的URL和方法,与Server期望的是否一致。
  • 示例 :如果你定义的命令是 (“GET”, “/session/:session_id/appium/device/info”) ,但Server实际端点可能是 /wd/hub/session/:session_id/appium/device/info (如果你把Appium Server放在 /wd/hub 路径下)。这时需要调整URL。

6.2 连接重试机制导致测试执行时间过长

  • 问题 :设置了重试后,某个本来会快速失败的命令(如元素找不到)现在会等待多次重试后才报错,拖慢了整体测试速度。
  • 解决 区分可重试异常和不可重试异常 。不是所有异常都值得重试。
    • 可重试异常 ConnectionRefusedError , TimeoutError , socket.error 等网络层异常,或WebDriverException中表示临时服务端错误的(如 UnknownError ,但需谨慎)。
    • 不可重试异常 NoSuchElementException (元素找不到)、 InvalidSelectorException (选择器错误)等业务逻辑错误,重试没有意义。
  • 优化重试逻辑 :修改 RetryableRemoteConnection.execute 方法,在 except 块中判断异常类型。
def execute(self, command, params=None):
    params = params or {}
    last_exception = None
    for attempt in range(self.retry_count + 1):
        try:
            return super().execute(command, params)
        except (socket.error, ConnectionRefusedError, TimeoutError) as e:
            # 网络相关异常,重试
            last_exception = e
            self.logger.warning(f"网络异常,第{attempt + 1}次重试...")
            if attempt < self.retry_count:
                time.sleep(self.retry_delay)
            else:
                raise
        except WebDriverException as e:
            # WebDriver异常,需要根据消息判断是否可重试
            if "unknown error" in str(e).lower() or "internal server error" in str(e).lower():
                # 可能是服务端临时错误,重试
                last_exception = e
                self.logger.warning(f"服务端异常,第{attempt + 1}次重试...")
                if attempt < self.retry_count:
                    time.sleep(self.retry_delay)
                else:
                    raise
            else:
                # 其他WebDriver异常(如NoSuchElement),直接抛出,不重试
                raise

6.3 日志过于冗长或找不到日志

  • 问题 :自定义连接类的日志没有输出,或者输出太多干扰信息。
  • 解决
    1. 配置Python Logging :在你的测试脚本入口或 conftest.py 中配置logging级别和格式。
      import logging
      logging.basicConfig(level=logging.DEBUG, 
                          format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
      # 如果只想看自定义连接的日志,可以设置特定logger的级别
      # logging.getLogger('custom_connection').setLevel(logging.DEBUG)
      
    2. 控制日志级别 :在 RetryableRemoteConnection 中,对于不同的操作使用不同的级别。 DEBUG 用于详细请求/响应, INFO 用于重要步骤, WARNING 用于重试, ERROR 用于最终失败。
    3. 使用日志文件 :将日志输出到文件,便于持续集成(CI)环境查看。
      file_handler = logging.FileHandler('appium_test.log')
      file_handler.setLevel(logging.DEBUG)
      formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
      file_handler.setFormatter(formatter)
      logging.getLogger().addHandler(file_handler)
      

6.4 与Page Object Model (POM) 模式结合

自定义的Driver如何优雅地用在POM中?很简单,在你的Page类中,直接使用这个自定义Driver即可。

# base_page.py
class BasePage:
    def __init__(self, driver: CustomAppiumDriver): # 类型注解提示使用自定义Driver
        self.driver = driver
        self.logger = logging.getLogger(self.__class__.__name__)
    
    def find_and_click(self, by, selector):
        """ 使用Driver的自定义等待点击方法 """
        return self.driver.wait_and_click(by, selector)

# login_page.py
class LoginPage(BasePage):
    USERNAME_FIELD = (AppiumBy.ID, 'com.example:id/username')
    PASSWORD_FIELD = (AppiumBy.ID, 'com.example:id/password')
    LOGIN_BUTTON = (AppiumBy.ID, 'com.example:id/login_btn')
    
    def login(self, username, password):
        self.driver.find_element(*self.USERNAME_FIELD).send_keys(username)
        self.driver.find_element(*self.PASSWORD_FIELD).send_keys(password)
        # 使用基类封装的方法,它内部调用了driver的自定义方法
        self.find_and_click(*self.LOGIN_BUTTON)
        # 也可以直接使用driver的其他自定义方法
        screen_info = self.driver.get_screen_info()
        self.logger.info(f"登录时屏幕信息: {screen_info}")

通过将自定义Driver作为Page类的依赖注入,你可以在整个页面对象体系中无缝使用所有扩展功能,保持代码的清晰和可维护性。

更多推荐