1. 项目概述:为什么需要异步测试自动化?

如果你正在用Python做RPA(机器人流程自动化),或者开发任何需要与外部系统(网页、API、数据库)频繁交互的脚本,那你肯定对“等待”深恶痛绝。传统的线性脚本,执行一个点击操作后,傻傻地等页面加载完,再去执行下一个查找元素的操作,大量的时间都浪费在I/O等待上。效率低只是表象,更麻烦的是,这种同步阻塞的模型,在编写自动化测试时尤其痛苦——一个步骤卡住,整个测试用例就挂了,排查起来像是大海捞针。

这就是我当初决定深入研究 RPA-Python与pytest-trio集成 的核心动因。我们团队维护着一个复杂的财务对账RPA流程,脚本需要同时操作本地Excel、查询多个Web API、并向数据库写入结果。最初用 requests + selenium + unittest 的同步组合,用例平均运行时间超过10分钟,且极不稳定。直到我们将核心逻辑用 Trio 这个现代异步库重写,并用 pytest 配合 pytest-trio 插件来组织测试,才真正解决了问题。现在,一套完整的集成测试能在2分钟内跑完,稳定性提升了不止一个量级。

简单来说,这个组合解决了RPA自动化测试中的几个核心痛点:

  1. 效率瓶颈 :异步并发可以同时等待多个I/O操作(如多个网络请求、多个页面元素加载),将空闲的CPU时间利用起来,大幅缩短测试执行时间。
  2. 测试稳定性 :Trio提供了结构化并发的原语,能更精细地控制超时、取消任务,避免了传统多线程/多进程中资源泄漏和状态混乱的问题,让测试环境更干净。
  3. 开发体验 :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}秒)")
        # 点击成功,可以继续后续逻辑

为什么这么设计?

  1. managed_session :使用 @asynccontextmanager 可以确保无论中间代码是否发生异常, session.close() 都会被调用,防止网络连接泄漏。这在测试中至关重要,一个未关闭的连接可能会影响下一个测试。
  2. 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_after vs fail_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
    # ... 更多业务断言

夹具的优势

  1. 代码复用 :登录逻辑写一次,所有需要登录的测试都能用。
  2. 作用域控制 :通过 scope 参数(如 scope="session" , scope="module" , scope="class" , scope="function" 默认),可以控制夹具创建和销毁的频率,优化测试速度。例如, browser 夹具用 scope="session" ,整个测试会话只启动一次浏览器,大大提速。
  3. 自动清理 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 调试技巧:在异步测试中设置断点

调试异步代码有时令人困惑。我的建议是:

  1. 使用 trio.run() 进行局部调试 :对于复杂的异步函数,可以写一个小的脚本,用 trio.run(main_function) 来直接运行和调试,排除测试框架的干扰。
  2. 在测试中使用 trio.sleep() 临时暂停 :在怀疑有问题的地方插入 await trio.sleep(0.1) ,有时可以缓解竞争条件,帮助你观察中间状态。
  3. 利用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/

这个配置做了几件关键事:

  1. 多版本测试 :确保你的代码在多个Python版本上都能工作。
  2. 快速安装 :使用 uv 加速依赖安装。
  3. 浏览器准备 :在CI环境中安装Playwright所需的浏览器。
  4. 运行测试 :使用详细输出和简短的错误回溯。 --junitxml 生成XML报告,方便与CI平台集成展示。
  5. 环境隔离 :通过环境变量传递测试配置(如数据库连接、是否无头模式)。

将这套流程融入你的开发习惯后,每次提交代码都能自动验证RPA逻辑的稳定性,从而在复杂的企业自动化项目中建立起坚实的质量防线。从手动点击验证到自动化、并发的测试套件,这种转变带来的效率和信心提升是巨大的。

更多推荐