Python异步RPA测试实战:pytest-trio集成提升自动化效率与稳定性
1. 项目概述:为什么需要异步测试自动化?
如果你正在用Python做RPA(机器人流程自动化),或者开发任何需要与外部系统(网页、API、数据库)频繁交互的脚本,那你肯定对“等待”深恶痛绝。传统的线性脚本,执行一个点击操作后,傻傻地等页面加载完,再去执行下一个查找元素的操作,大量的时间都浪费在I/O等待上。效率低只是表象,更麻烦的是,这种同步阻塞的模型,在编写自动化测试时尤其痛苦——一个步骤卡住,整个测试用例就挂了,排查起来像是大海捞针。
这就是我当初决定深入研究 RPA-Python与pytest-trio集成 的核心动因。我们团队维护着一个复杂的财务对账RPA流程,脚本需要同时操作本地Excel、查询多个Web API、并向数据库写入结果。最初用 requests + selenium + unittest 的同步组合,用例平均运行时间超过10分钟,且极不稳定。直到我们将核心逻辑用 Trio 这个现代异步库重写,并用 pytest 配合 pytest-trio 插件来组织测试,才真正解决了问题。现在,一套完整的集成测试能在2分钟内跑完,稳定性提升了不止一个量级。
简单来说,这个组合解决了RPA自动化测试中的几个核心痛点:
- 效率瓶颈 :异步并发可以同时等待多个I/O操作(如多个网络请求、多个页面元素加载),将空闲的CPU时间利用起来,大幅缩短测试执行时间。
- 测试稳定性 :Trio提供了结构化并发的原语,能更精细地控制超时、取消任务,避免了传统多线程/多进程中资源泄漏和状态混乱的问题,让测试环境更干净。
- 开发体验 :pytest提供了强大的夹具(fixture)、参数化、钩子等机制,而pytest-trio让你能在测试函数中直接使用
async def和Trio的异步上下文,写起测试来非常自然流畅。
接下来的内容,我会从一个完整的实战项目出发,手把手带你搭建环境、编写核心异步RPA操作、设计pytest测试用例,并分享我们趟过的所有坑和总结的最佳实践。无论你是想优化现有RPA项目的测试,还是为新的异步Python项目构建测试体系,这份指南都能提供直接的参考。
2. 环境搭建与核心工具链解析
工欲善其事,必先利其器。这套技术栈的搭建,关键在于让pytest、trio和你的RPA工具(如playwright、pyautogui)和谐共处。下面是我经过多次踩坑后总结的最稳定、高效的配置方案。
2.1 Python环境与依赖管理
强烈建议使用 Python 3.8及以上版本 ,因为对异步语法的支持更加成熟和完善。我个人的首选是使用 uv 或 pdm 这类现代、快速的包管理工具来创建虚拟环境和管理依赖,它们比传统的 venv + pip 组合要快得多。
# 使用uv初始化项目(如果没有uv,可以用 pipx install uv 安装)
uv init rpa-async-test-project
cd rpa-async-test-project
# 添加核心依赖
uv add pytest pytest-trio trio
# 根据你的RPA场景添加驱动库,例如:
uv add playwright # 用于浏览器自动化
uv add pyautogui # 用于桌面GUI自动化
uv add aiohttp httpx # 用于异步HTTP请求
uv add sqlalchemy asyncpg # 用于异步数据库操作
# 安装playwright的浏览器驱动
uv run playwright install chromium
这里解释一下几个关键依赖的选择理由:
- pytest-trio :这是连接pytest和trio的桥梁。它主要做两件事:一是允许你编写
async def test_...格式的测试函数;二是为每个测试函数提供一个精心管理好的Trio运行环境(nursery),测试结束后会自动清理所有后台任务,这是保证测试隔离性的关键。 - trio :我们选择的异步运行时库。相比于asyncio,Trio的“结构化并发”理念更严格,它强制要求所有后台任务都必须在一个“育儿室”(nursery)中启动,并且父任务会等待所有子任务结束。这从根本上避免了“孤儿任务”导致的资源泄漏,对于需要稳定、可预测的测试环境来说,这是巨大的优势。
- playwright :为什么选它而不是selenium?除了性能更好、API更现代外,Playwright对异步的原生支持是一流的。它的几乎所有方法都是
async的,可以与Trio无缝协作,直接await page.click('button'),代码非常直观。
注意 :如果你在Windows上使用pyautogui等涉及全局键盘/鼠标监听的库,在异步环境中要格外小心。这些库通常是阻塞式的,可能会卡住整个事件循环。建议将它们的操作封装到
trio.to_thread.run_sync中,丢到线程池里执行,避免阻塞主循环。
2.2 项目结构设计
一个清晰的项目结构,能让测试代码和产品代码都更易于维护。这是我推荐的结构:
rpa-async-test-project/
├── pyproject.toml # 项目配置和依赖声明(uv/pdm使用)
├── src/ # 源代码目录
│ └── rpa_bot/ # 你的RPA核心模块
│ ├── __init__.py
│ ├── async_operations.py # 核心异步操作(如登录、查询、点击)
│ └── models.py # 数据模型
├── tests/ # 测试目录
│ ├── __init__.py
│ ├── conftest.py # pytest全局配置文件,放置公共fixture
│ ├── test_async_ops.py # 针对核心异步操作的单元测试
│ └── test_integration.py # 集成测试
└── examples/ # 示例脚本
关键在于 conftest.py ,这里是配置pytest-trio和定义公共夹具的地方。一个基础的配置如下:
# tests/conftest.py
import pytest
import trio
from playwright.async_api import async_playwright
# 这是pytest-trio需要的,它告诉pytest如何运行异步测试函数
pytest_plugins = ['pytest_trio']
# 定义一个会话级别的fixture,用于在整个测试会话中只启动一次浏览器
@pytest.fixture(scope="session")
async def browser():
async with async_playwright() as p:
# 通常测试使用headless模式,更快且不干扰
browser = await p.chromium.launch(headless=True)
yield browser
await browser.close()
# 定义一个函数级别的fixture,每个测试函数获得一个干净的页面
@pytest.fixture
async def page(browser):
page = await browser.new_page()
yield page
await page.close()
# 一个通用的Trio nursery fixture,方便在测试中启动后台任务
@pytest.fixture
def nursery():
# 实际上,pytest-trio已经为每个async测试函数提供了内置的nursery
# 这个fixture更多是教学目的,展示如何显式获取
# 在测试函数中,你可以通过 `request.getfixturevalue('nursery')` 获取,但通常直接使用async函数上下文即可。
pass
3. 核心异步RPA操作封装与原理
有了环境,我们来深入核心:如何用Trio编写健壮的异步RPA操作。RPA任务本质是一系列I/O密集型操作,异步化改造的核心思路是: 将每一个“等待”的点,都变成一个 await 表达式 。
3.1 异步上下文管理器与资源管理
在异步世界里,资源管理(如打开/关闭浏览器、连接/断开数据库)不能再依赖 __del__ 或手动调用,而必须使用 async with 上下文管理器。Trio和很多现代异步库都深度支持这一模式。
# src/rpa_bot/async_operations.py
import trio
from contextlib import asynccontextmanager
from playwright.async_api import Page
class AsyncRPAClient:
def __init__(self):
self._session = None # 假设是aiohttp session
self._page = None
@asynccontextmanager
async def managed_session(self, base_url):
"""一个用于管理HTTP会话的异步上下文管理器"""
import aiohttp
session = aiohttp.ClientSession(base_url=base_url)
self._session = session
try:
yield session
finally:
await session.close()
self._session = None
async def safe_click(self, page: Page, selector: str, timeout: float = 10.0):
"""
一个安全的点击操作封装。
核心:合并了等待元素可见/可点击和实际点击,并添加超时控制。
"""
with trio.move_on_after(timeout) as cancel_scope:
# 1. 等待元素出现并可见
element = await page.wait_for_selector(
selector, state="visible", timeout=timeout*1000 # playwright用毫秒
)
# 2. 确保元素是可点击的(例如,没有被遮挡、disabled属性为false)
# 这里可以添加自定义逻辑,比如检查element的某些属性
# 3. 执行点击
await element.click()
if cancel_scope.cancelled_caught:
raise TimeoutError(f"点击元素 {selector} 超时({timeout}秒)")
# 点击成功,可以继续后续逻辑
为什么这么设计?
-
managed_session:使用@asynccontextmanager可以确保无论中间代码是否发生异常,session.close()都会被调用,防止网络连接泄漏。这在测试中至关重要,一个未关闭的连接可能会影响下一个测试。 -
safe_click:这是RPA测试中的黄金函数。它用trio.move_on_after包裹了整个点击流程,设定了硬性超时。这意味着,即使页面卡死、元素永不出现,你的测试也不会无限期挂起,而是在超时后抛出清晰的异常,方便快速定位问题。wait_for_selector的state="visible"比单纯的wait_for_selector更可靠,因为它要求元素不仅存在于DOM,还要在视觉上可交互。
3.2 结构化并发:同时执行多个RPA任务
这是Trio的杀手锏。假设你的RPA流程需要从三个不同的API获取数据,然后合并处理。同步代码只能顺序执行,而用Trio可以轻松并发。
async def fetch_data_from_multiple_sources(self, urls):
"""并发从多个源获取数据"""
async with trio.open_nursery() as nursery:
# 用于存放结果的字典
results = {}
for url in urls:
# 为每个url启动一个子任务
nursery.start_soon(self._fetch_single_url, url, results)
# open_nursery上下文块会等待所有start_soon启动的任务完成
return results
async def _fetch_single_url(self, url, result_dict):
"""子任务:获取单个URL的数据"""
try:
async with self._session.get(url) as resp:
data = await resp.json()
result_dict[url] = data
except Exception as e:
# 妥善处理单个任务失败,不影响其他任务
result_dict[url] = {"error": str(e)}
# 可以选择记录日志,或者根据测试需求决定是否让整个nursery失败
# 如果希望一个失败就全部停止,可以在这里 raise
关键点解析 :
open_nursery()创建了一个“育儿室”,所有在里面start_soon的任务都是它的孩子。- 父任务(
fetch_data_from_multiple_sources)会一直阻塞在async with块内,直到所有子任务完成。这保证了资源的生命周期管理。 - 如果任何一个子任务抛出未捕获的异常,整个nursery会立即取消,所有其他子任务也会被取消。这种“一损俱损”的默认行为在测试中通常是安全的,防止了部分任务失败后系统处于未知状态。如果你希望某些任务失败不影响其他任务,必须在子任务内部做好异常捕获,就像示例中那样。
3.3 超时与取消:让测试更可靠
不稳定的网络、无响应的第三方服务是RPA测试的常态。我们必须为所有外部操作设置合理的超时。
async def robust_form_submission(self, page: Page, form_data: dict):
"""
一个健壮的表单提交函数,集成了多个超时控制点。
"""
# 全局超时:整个表单提交流程不能超过30秒
with trio.move_on_after(30.0) as global_cancel_scope:
# 步骤1:填写表单,每个字段填写有独立超时
for field, value in form_data.items():
with trio.move_on_after(5.0):
await page.fill(f'input[name="{field}"]', value)
if global_cancel_scope.cancelled_caught:
raise TimeoutError("表单填写过程超时")
# 步骤2:提交按钮点击,超时10秒
with trio.move_on_after(10.0):
await self.safe_click(page, 'button[type="submit"]')
# 步骤3:等待提交成功后的跳转或提示,超时15秒
with trio.move_on_after(15.0):
# 等待一个成功提示元素出现
await page.wait_for_selector('.alert-success', state='visible', timeout=15000)
if global_cancel_scope.cancelled_caught:
# 这里可以记录更详细的日志,比如是在哪个阶段超时的
raise TimeoutError("表单提交整体流程超时")
return True
实操心得 :
- 分层超时 :如示例所示,为整个流程设置一个全局超时,再为每个关键子步骤设置更短的局部超时。这样当测试失败时,你能快速定位是“网络慢”还是“页面逻辑卡在了某个特定环节”。
move_on_aftervsfail_after:trio.move_on_after超时后是取消当前作用域内的代码,并继续执行后面的代码(如果还有的话)。而trio.fail_after超时会直接抛出trio.TooSlowError异常。在测试中,move_on_after配合检查cancel_scope.cancelled_caught更灵活,便于清理资源或记录错误状态。
4. 使用pytest-trio编写高效测试用例
现在,我们的异步RPA操作已经准备好了,是时候用pytest来测试它们了。pytest-trio让这一切变得异常简单。
4.1 编写第一个异步测试
假设我们要测试上面写的 safe_click 函数。
# tests/test_async_ops.py
import pytest
from src.rpa_bot.async_operations import AsyncRPAClient
from playwright.async_api import Page
@pytest.mark.trio # 这个装饰器是必须的,它告诉pytest这个测试函数是异步的,需要用trio运行
async def test_safe_click_success(page: Page):
"""测试safe_click在元素正常存在时的行为"""
client = AsyncRPAClient()
# 首先,导航到一个我们知道有特定按钮的页面(可以是本地测试HTML文件)
await page.goto('file:///path/to/your/test_page.html')
# 执行点击
await client.safe_click(page, '#test-button', timeout=5.0)
# 断言点击后的结果。例如,点击后应该出现一个提示框。
success_message = await page.text_content('#message')
assert success_message == "点击成功!"
# 或者断言某个元素的状态发生了变化
button = await page.query_selector('#test-button')
assert await button.get_attribute('disabled') == 'true'
@pytest.mark.trio
async def test_safe_click_timeout(page: Page):
"""测试safe_click在元素不出现时的超时行为"""
client = AsyncRPAClient()
await page.goto('file:///path/to/your/test_page.html')
# 尝试点击一个根本不存在的元素
with pytest.raises(TimeoutError) as exc_info:
await client.safe_click(page, '#non-existent-button', timeout=2.0) # 设置一个很短的超时
# 断言异常信息符合预期
assert "点击元素 #non-existent-button 超时" in str(exc_info.value)
要点说明 :
@pytest.mark.trio:这是 灵魂所在 。没有它,pytest会把async def函数当作普通函数返回一个协程对象,而不会去执行它。加上这个标记,pytest-trio插件会接管,为这个测试函数创建一个干净的Trio事件循环来运行它。- 夹具自动注入 :注意测试函数参数
page: Page。pytest会自动识别这个类型,并从我们之前在conftest.py中定义的page夹具注入一个全新的、独立的浏览器页面对象。这保证了测试之间的隔离。 - 异步断言 :断言本身是同步的,但断言的对象(如
page.text_content()的返回值)可能需要await。确保在获取断言目标时使用了await。
4.2 使用夹具管理复杂测试状态
对于需要登录状态、特定数据库数据等复杂场景的测试,夹具是你的最佳工具。
# tests/conftest.py (追加)
import pytest
import trio
from src.rpa_bot.async_operations import AsyncRPAClient
@pytest.fixture
async def logged_in_client(page: Page):
"""返回一个已登录的RPA客户端实例"""
client = AsyncRPAClient()
# 执行登录流程
await page.goto("https://example.com/login")
await page.fill('#username', 'test_user')
await page.fill('#password', 'test_pass')
await client.safe_click(page, '#login-btn')
# 等待登录成功,例如跳转到首页
await page.wait_for_url('**/dashboard')
# 将登录后的page绑定到client(假设client需要)
client._page = page
yield client
# 清理:登出(如果需要)
# await client.logout()
# tests/test_integration.py
@pytest.mark.trio
async def test_dashboard_operations(logged_in_client):
"""测试登录后才能进行的操作"""
client = logged_in_client
# 现在client已经处于登录状态,可以直接测试后续业务
data = await client.fetch_dashboard_data()
assert len(data['widgets']) > 0
# ... 更多业务断言
夹具的优势 :
- 代码复用 :登录逻辑写一次,所有需要登录的测试都能用。
- 作用域控制 :通过
scope参数(如scope="session",scope="module",scope="class",scope="function"默认),可以控制夹具创建和销毁的频率,优化测试速度。例如,browser夹具用scope="session",整个测试会话只启动一次浏览器,大大提速。 - 自动清理 :
yield之后的代码就是清理逻辑,无论测试成功还是失败,pytest都会确保执行清理,保证测试环境干净。
4.3 参数化测试与数据驱动
RPA测试经常需要对同一套流程测试不同的输入数据。pytest的 @pytest.mark.parametrize 可以完美结合。
# tests/test_async_ops.py
import pytest
@pytest.mark.trio
@pytest.mark.parametrize("username, password, expected_success", [
("correct_user", "correct_pass", True),
("wrong_user", "correct_pass", False),
("correct_user", "", False), # 空密码
("", "correct_pass", False), # 空用户名
])
async def test_login_with_parameters(page: Page, username, password, expected_success):
"""使用多组数据测试登录功能"""
client = AsyncRPAClient()
await page.goto("https://example.com/login")
await page.fill('#username', username)
await page.fill('#password', password)
await client.safe_click(page, '#login-btn')
if expected_success:
# 期望成功:应该跳转到dashboard
await page.wait_for_url('**/dashboard', timeout=10.0)
assert '/dashboard' in page.url
else:
# 期望失败:应该停留在登录页或出现错误提示
# 可能页面不会跳转,或者会出现错误信息
error_element = await page.query_selector('.error-message')
assert error_element is not None
error_text = await error_element.text_content()
assert len(error_text) > 0
运行这个测试,pytest会自动展开成4个独立的测试用例来执行,并在报告中清晰显示每条数据对应的结果。这对于测试边界条件和异常流非常高效。
5. 高级技巧与实战问题排查
掌握了基础,我们来看看一些能让你事半功倍的高级技巧,以及如何解决那些令人头疼的常见问题。
5.1 模拟(Mock)与依赖注入
测试不应该依赖不稳定的外部服务。对于HTTP API、数据库调用,我们需要用模拟对象(Mock)来替代。
import pytest
from unittest.mock import AsyncMock, patch
from src.rpa_bot.async_operations import AsyncRPAClient
@pytest.mark.trio
async def test_fetch_data_with_mock():
"""使用AsyncMock模拟网络请求"""
client = AsyncRPAClient()
# 创建一个模拟的aiohttp.ClientSession.get方法返回的上下文管理器
mock_response = AsyncMock()
mock_response.__aenter__.return_value = mock_response
mock_response.json.return_value = {"id": 123, "name": "Mock Item"}
mock_response.status = 200
mock_session = AsyncMock()
mock_session.get.return_value = mock_response
# 使用patch临时替换client的_session属性
with patch.object(client, '_session', mock_session):
# 现在client内部的任何_get请求都会使用我们的mock
# 假设client有一个内部方法调用_session.get
result = await client._fetch_some_internal_data("https://api.example.com/item/123")
# 断言调用了正确的URL
mock_session.get.assert_called_once_with("https://api.example.com/item/123")
# 断言返回了模拟的数据
assert result["name"] == "Mock Item"
为什么用 AsyncMock ? 因为我们要模拟的是异步方法( __aenter__ , json() 等)。 unittest.mock.AsyncMock 能正确处理 await 调用。这是测试异步代码时最容易踩的坑之一——用普通的 Mock 去模拟异步方法,会导致 TypeError: object Mock can't be used in 'await' expression 。
5.2 测试超时与并行执行配置
默认情况下,pytest会顺序执行测试。但对于大量异步测试,我们可以利用其并行功能加速。
首先,安装并行插件:
uv add pytest-xdist
然后,在运行测试时指定并行进程数:
uv run pytest -n auto tests/ # 使用与CPU核心数相同的worker
uv run pytest -n 2 tests/ # 使用2个worker
重要警告 :并行测试时, 必须确保测试是完全独立的 ,不能共享任何全局状态(如全局变量、同一个浏览器实例、同一个文件句柄)。我们的 page 夹具是 function 作用域的,每个测试都会得到全新的页面,所以是安全的。但如果你有 session 或 module 作用域的夹具操作了共享资源(比如向同一个测试数据库写入数据),就需要用锁或为每个worker创建独立命名空间来隔离。
此外,可以为单个测试或所有测试设置全局超时,防止某个用例卡死整个套件。
# 在conftest.py中为所有测试添加全局超时
def pytest_collection_modifyitems(items):
for item in items:
if 'trio' in item.keywords: # 仅为trio测试添加超时
item.add_marker(pytest.mark.timeout(30)) # 每个测试最多30秒
# 或者使用装饰器为单个测试设置
import pytest
@pytest.mark.trio
@pytest.mark.timeout(10) # 这个测试最多10秒
async def test_quick_operation():
...
5.3 常见问题排查实录
以下是我们团队在实战中遇到的一些典型问题及解决方案:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 测试通过,但pytest报告有“未清理的协程”或“任务仍在运行”警告。 | 测试函数中启动了后台任务(如通过 nursery.start_soon ),但测试结束前没有等待它们完成。 |
方案1 :确保所有 start_soon 的任务都在 async with open_nursery() 块内启动,该块会等待所有子任务。 方案2 :如果必须在测试函数内启动独立任务,请使用 async with trio.open_nursery() as nursery: 并在测试结尾 await nursery.cancel_scope.cancel() 显式取消。但最佳实践是避免在测试函数内直接管理nursery,而是通过被测函数内部的nursery来管理。 |
RuntimeError: Task attached to a different loop |
在Trio的事件循环中,混用了为asyncio事件循环创建的对象(如某个库创建的aiohttp session)。 | 确保所有异步对象都在同一个事件循环中创建。对于第三方库,检查其是否支持Trio。如果不支持,考虑使用 trio_asyncio 模块(一个允许在Trio中运行asyncio代码的桥接库),或者寻找替代库。对于Playwright,使用 async_playwright 而不是 sync_playwright ,并确保 browser 和 page 对象都在同一个async上下文中创建和使用。 |
| 测试随机失败,报错“元素未找到”或“超时”。 | 1. 页面加载或元素渲染比预期慢(网络/性能波动)。 2. 元素定位器不稳定(如使用了易变的XPath或CSS选择器)。 3. 异步操作竞争条件(如点击后未等待足够时间就进行断言)。 |
排查 : 1. 增加超时 :适当增加 wait_for_selector 或 move_on_after 的超时时间。 2. 优化定位器 :使用Playwright推荐的 data-testid 属性,或更稳定的CSS选择器(如 button:has-text("Submit") )。 3. 显式等待 :在关键操作后,使用 page.wait_for_function 等待某个JavaScript条件成立,或 page.wait_for_url 等待页面跳转完成,再进行断言。 4. 启用日志 :在pytest运行时添加 -s 和 -v 参数,并配置Playwright日志( PWDEBUG=1 )来查看详细操作步骤。 |
使用 pytest-xdist 并行时,测试互相干扰,数据混乱。 |
测试用例之间没有完全隔离,共享了数据库、文件或浏览器上下文。 | 方案 : 1. 数据库 :为每个测试worker使用独立的数据库(如通过环境变量指定不同的数据库名),或在每个测试用例开始前回滚事务。 2. 文件 :使用临时文件目录( tmp_path 夹具),确保每个测试写入不同的文件。 3. 浏览器 :确保 browser 夹具是 session 作用域,但 page 或 context 夹具是 function 作用域,这样每个测试都有独立的页面标签页。 |
async 测试函数被跳过,提示“不支持异步”。 |
忘记添加 @pytest.mark.trio 装饰器。 |
为所有 async def test_... 函数加上 @pytest.mark.trio 装饰器。 |
5.4 调试技巧:在异步测试中设置断点
调试异步代码有时令人困惑。我的建议是:
- 使用
trio.run()进行局部调试 :对于复杂的异步函数,可以写一个小的脚本,用trio.run(main_function)来直接运行和调试,排除测试框架的干扰。 - 在测试中使用
trio.sleep()临时暂停 :在怀疑有问题的地方插入await trio.sleep(0.1),有时可以缓解竞争条件,帮助你观察中间状态。 - 利用Playwright的录制和追踪 :Playwright可以录制操作生成代码,也可以生成详细的追踪文件。在测试失败时,保存追踪文件并用Playwright Trace Viewer打开,能清晰地看到每一步操作、网络请求和页面快照,是定位问题的神器。
# 在fixture或测试中启用追踪 browser = await p.chromium.launch() context = await browser.new_context() # 启动追踪 await context.tracing.start(screenshots=True, snapshots=True, sources=True) # ... 执行测试 ... # 测试失败时保存追踪 await context.tracing.stop(path="trace.zip")
6. 构建持续集成(CI)流水线
最后,让这套测试在CI/CD流水线中自动运行,才能最大化其价值。这里给出一个GitHub Actions的配置示例。
# .github/workflows/test.yml
name: RPA Async Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"] # 测试多版本兼容性
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install uv (快速包管理工具)
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Install dependencies
run: |
uv sync --all-extras --dev # 安装所有依赖,包括开发依赖
- name: Install Playwright Browsers
run: |
uv run playwright install chromium --with-deps
- name: Run tests with pytest
run: |
uv run pytest tests/ -v --tb=short --junitxml=junit/test-results-${{ matrix.python-version }}.xml
env:
# 设置无头模式和环境变量
HEADLESS: "true"
# 如果有测试数据库,可以在这里设置连接字符串
TEST_DATABASE_URL: "sqlite:///./test.db"
- name: Upload test results (optional)
if: always() # 即使测试失败也上传报告
uses: actions/upload-artifact@v3
with:
name: test-results-${{ matrix.python-version }}
path: junit/
这个配置做了几件关键事:
- 多版本测试 :确保你的代码在多个Python版本上都能工作。
- 快速安装 :使用
uv加速依赖安装。 - 浏览器准备 :在CI环境中安装Playwright所需的浏览器。
- 运行测试 :使用详细输出和简短的错误回溯。
--junitxml生成XML报告,方便与CI平台集成展示。 - 环境隔离 :通过环境变量传递测试配置(如数据库连接、是否无头模式)。
将这套流程融入你的开发习惯后,每次提交代码都能自动验证RPA逻辑的稳定性,从而在复杂的企业自动化项目中建立起坚实的质量防线。从手动点击验证到自动化、并发的测试套件,这种转变带来的效率和信心提升是巨大的。
更多推荐


所有评论(0)