【Appium 系列】第08节-pytest 集成 — conftest.py 中的 fixture 与 hook

配套代码:配套代码/test/conftest.py


conftest.py 是干什么的

pytest 有个约定:conftest.py 里的东西,对同目录和子目录下的所有测试文件自动生效。你不用写 import,不用到处复制代码,pytest 自己就能找到它。

这就很适合放一些"每个测试用例都可能用到的东西"------比如 Appium 的 driver、API 客户端、各个 hook 回调。简单说,conftest.py 就是测试框架的骨架,测试用例只管写业务逻辑,基础设施的事全交给 conftest 处理。


session 级 driver fixture

Appium driver 初始化一次要十秒起步,100 个用例每个都 new 一个 driver,光启动就半小时。所以 driver 肯定是 session 级别的------整个测试跑下来只创建一次。

复制代码
@pytest.fixture(scope="session")
def driver_setup(request):
    webdriver, BaseDriver = _get_appium()
    if BaseDriver is None:
        pytest.skip("Appium 未安装,跳过 UI 测试")

    platform = os.getenv("PLATFORM", "android")
    logger.info(f"开始初始化移动端驱动 - 平台: {platform}")

    base_driver = BaseDriver(platform=platform)
    driver = base_driver.get_driver()

    def driver_teardown():
        try:
            driver.quit()
            logger.info("驱动关闭成功")
        except Exception as e:
            logger.error(f"驱动关闭异常: {str(e)}")

    request.addfinalizer(driver_teardown)
    yield driver

scope="session" 的意思是:pytest 只调一次这个 fixture,所有测试用例共用同一个 driver。

注意那个 _get_appium()------它做了延迟导入。如果机器上没装 Appium,不会一启动就挂掉,而是跳过所有 UI 测试。这个在实际项目里很实用,CI 环境里经常只跑非 UI 的测试。

request.addfinalizer(driver_teardown) 注册了一个清理函数。不管测试成功还是失败,甚至中间崩了,这个 finalizer 一定会执行。用 yield 后面跟代码也能做 teardown,但 finalizer 更可靠------yield 后面的代码在 fixture 内部异常时可能被跳过,finalizer 不会。


function 级 driver fixture

session 级 fixture 只初始化一次,但有时候你需要在每个用例执行完做点事------比如截个图。

复制代码
@pytest.fixture(scope="function")
def driver(request, driver_setup):
    yield driver_setup

这个 fixture 直接依赖 driver_setup,pytest 会自动解析依赖关系:先执行 session 级的 driver_setup,再把这个结果传给 function 级的 driver

看起来好像啥也没干,就是透传了一下。但它给未来留了个口子------如果哪天你想在每个用例结束后截个图、清个缓存、重置 App 到首页,直接在这里加代码就行,不用改测试用例里的任何东西。


API 客户端 fixture

UI 测试经常需要配合 API 做数据准备或结果校验。

复制代码
@pytest.fixture(scope="session")
def api_client(request):
    from api.base_api import BaseAPI

    base_url = os.getenv("API_BASE_URL", "")
    timeout = int(os.getenv("API_TIMEOUT", "30"))
    client = BaseAPI(base_url=base_url, timeout=timeout)

    api_token = os.getenv("API_TOKEN", "")
    if api_token:
        client.set_auth("bearer", api_token)

    yield client

同样是 session 级别,整个测试过程只创建一次。用环境变量来控制 API 地址和 token,测试代码里不硬编码,换环境时改环境变量就行。


hook 收集测试结果

fixture 拿不到测试执行的结果。fixture 执行的时候,测试用例还没跑(setup 阶段),或者已经跑完了(teardown 阶段),但 fixture 自己不知道测试过了还是没过。

hook 可以。

复制代码
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    rep = outcome.get_result()
    setattr(item, f"rep_{rep.when}", rep)

这个 hook 做的事情不多:pytest 每执行一次 setup/call/teardown,它就把结果报告挂在 item 对象上。比如 item.rep_call 就能拿到测试函数执行的结果,里面包含 failed/passed 状态。

tryfirst=True 保证这个 hook 最早执行------其他插件或 fixture 要想读取 rep_call,得等它先挂上去。hookwrapper=True 的意思是"包一层":yield 之前是 setup 阶段的代码,yield 之后拿到的 outcome.get_result() 才是执行结果。

这样 fixture 就能这么用了:

复制代码
# 在 function 级 driver fixture 的 teardown 里
if hasattr(request.node, "rep_call") and request.node.rep_call.failed:
    # 取个截图
    screenshot_helper.take_screenshot(f"failure_{request.node.name}")

也就是:测试失败了 → 自动截图。不用在每个测试用例里写 try/except,全都由 conftest 统一处理。


autouse fixture:自动打日志

复制代码
@pytest.fixture(autouse=True)
def log_test_info(request):
    logger.info(f"开始执行测试用例: {request.node.name}")
    yield
    logger.info(f"测试用例执行完成: {request.node.name}")

autouse=True 意味着所有测试用例自动执行这个 fixture,不需要显式声明参数。每个用例前后各打一行日志,CI 里排错时很有用,能看清楚跑到了哪个用例、卡在了哪里。


整个流程串起来

一个测试用例的完整生命周期是这样的:

复制代码
pytest 启动
  ↓
session 级 driver_setup → 初始化 driver(只一次)
  ↓
session 级 api_client → 创建 API 客户端(只一次)
  ↓
对每个测试用例:
  ① autouse→log_test_info → 打印开始日志
  ② driver fixture(function 级)→ 拿到 driver
  ③ 执行测试函数
  ④ pytest_runtest_makereport hook → 把执行结果挂到 item 上
  ⑤ driver fixture teardown → 如果失败可以截图
  ⑥ autouse→log_test_info → 打印结束日志
  ↓
session 结束 → driver_setup 的 finalizer → driver.quit()

常见坑

request.addfinalizeryield 后面放代码更可靠。 yield 后面的代码如果 fixture 内部抛了异常可能会跳过,finalizer 不管怎样都会执行。清理 driver 这类操作,用 finalizer。

hook 的异常不会影响测试。 hook 里抛异常,pytest 只打一行警告,测试该过过该挂挂。但你的截图、报告生成这些功能就没了。所以 hook 里的操作最好包 try/except。

tryfirst=Truetrylast=True 的含义。 tryfirst 保证这个 hook 比其他同类型的先执行,适用于"我要先设置一些东西供别人使用"。trylast 则相反。

autouse 不要放耗时操作。 每个用例都跑一次,如果里面做了网络请求或文件读写,跑 500 个用例就多了 500 次。


几点总结

conftest.py 的核心价值就是四个字:分离关注。测试用例只管"测什么"和"怎么测",driver 从哪来、失败怎么办、结果怎么收集,conftest 处理。

实际项目里 conftest.py 可以很大------有人把 fixture、hook、工具函数全塞一个文件里,跑着也没问题。但如果 conftest.py 超过两三百行,可以考虑拆分:fixture 放一个文件,hook 放一个文件,用 conftest.py 统一 import 进来。不过这是口味问题,不是硬性规定。

相关推荐
来让爷抱一个1 小时前
MonkeyCode 多模型切换技巧:什么时候用 Claude/GPT/DeepSeek
人工智能·ai编程
李白你好2 小时前
AI Agent 架构的自动化渗透测试工具
运维·人工智能·自动化
许彰午2 小时前
14_Java泛型完全指南
java·windows·python
2601_949499942 小时前
8 大工业光模块供应商选型:芯瑞科技 400G OSFP 助力 AI 算力集群升级
人工智能·科技
温柔只给梦中人2 小时前
NLP学习:注意力机制
人工智能·学习·自然语言处理
广州灵眸科技有限公司2 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) Easy-Eai编译环境准备与更新
服务器·前端·人工智能·python·深度学习
深度学习lover2 小时前
<数据集>yolo樱桃识别<目标检测>
人工智能·深度学习·yolo·目标检测·计算机视觉·数据集·樱桃识别
深圳市机智人激光雷达2 小时前
技术筑牢安全冗余:激光雷达在自动驾驶高阶感知中的底层价值与范式演进
人工智能·安全·机器学习·3d·机器人·自动驾驶·无人机
江澎涌2 小时前
拆解与 AI 的一次对话
人工智能·算法·程序员
lqqjuly3 小时前
神经架构搜索深度解析(Neural Architecture Search, NAS)
人工智能·知识图谱