从一行 CSV 到一次浏览器操作:关键字驱动执行引擎设计

从一行 CSV 到一次浏览器操作:关键字驱动执行引擎设计

摘要

上一篇文章讲了第一版框架的数据模型:devices.csvcases.csvcase_steps.csvelements.yaml 分别负责什么。

但配置文件本身不会执行测试。

CSV 里写了一行 click,浏览器为什么真的会点击按钮?CSV 里写了一行 wait_request,框架为什么能等到接口并做断言?CSV 里写了一行 extract,变量又是怎么被保存起来的?

这篇文章就顺着一行 CSV 往下追,看看它是怎么一步步变成真实浏览器操作的。

CSV 只是剧本,框架负责执行

先看一行真实步骤。

csv 复制代码
GUEST_001,3,click init,click,init_button,,,,2,true

这行数据的意思是:

text 复制代码
用例:GUEST_001
步骤:第 3 步
名称:click init
动作:click
目标:init_button
依赖:第 2 步
失败后继续:true

测试同学维护用例时,不需要写:

python 复制代码
page.locator("#initSDK").click()

而是写:

csv 复制代码
click,init_button

init_button 再到 elements.yaml 里找真正的选择器:

yaml 复制代码
sdk_test_page:
  init_button: "#initSDK"

这就是关键字驱动的核心:CSV 不直接写 Python 代码,只写稳定的动作语义。Python 框架负责把这些语义翻译成 Playwright 操作。

pytest 是统一入口

当前框架没有为每条业务用例都写一个 Python 测试函数。

它只有一个统一入口:tests/test_csv_runner.py

核心流程可以简化成这样:

python 复制代码
devices = load_devices()
cases = load_cases()
steps_by_case = load_steps()
elements = load_elements()

selected_case_ids = _select_cases(pytestconfig, cases)
run_order = DependencyResolver(cases).resolve(selected_case_ids)

with sync_playwright() as playwright:
    browser_manager = BrowserManager(playwright, headless=...)
    runner = CaseRunner(browser_manager, devices, steps_by_case, elements, variables, logger)

    for case_id in run_order:
        result = runner.run_case(cases[case_id])
        collector.add_case_result(result)

这段代码解决了几个问题:

  • 从 CSV / YAML 读取配置。
  • 根据命令行参数选择要跑哪些用例。
  • 根据用例依赖算出实际执行顺序。
  • 创建 Playwright。
  • 把具体用例交给 CaseRunner 执行。
  • 最后统一收集结果。

所以当你运行:

bash 复制代码
python -m pytest tests/test_csv_runner.py --case-id GUEST_001

pytest 实际跑的不是一段写死的游客登录脚本,而是"CSV 驱动的通用执行器"。

CSV 先被读成配置对象

框架不会直接拿字符串到处传。

framework/config/csv_loader.py 会把 CSV 行转换成更明确的数据对象。

比如步骤会被读成 StepConfig

python 复制代码
@dataclass(frozen=True)
class StepConfig:
    case_id: str
    step_id: int
    step_name: str
    action: str
    target: str
    value: str
    expect: str
    save_as: str
    depends_on_step: tuple[int, ...]
    continue_on_fail: bool

case_steps.csv 里的每一列,基本都能在这里找到对应字段。

这一步很重要。

如果框架一直传原始 CSV 字符串,后面的执行逻辑会很乱。现在每个步骤都有明确字段:动作看 action,目标看 target,输入看 value,预期看 expect,要保存变量就看 save_as

加载步骤的代码大致是:

python 复制代码
step = StepConfig(
    case_id=row["case_id"].strip(),
    step_id=int(row["step_id"]),
    step_name=row["step_name"].strip(),
    action=row["action"].strip(),
    target=row["target"].strip(),
    value=row["value"].strip(),
    expect=row["expect"].strip(),
    save_as=row["save_as"].strip(),
    depends_on_step=_split_ints(row["depends_on_step"]),
    continue_on_fail=_as_bool(row["continue_on_fail"]),
)

测试同学看这段代码时,可以把它理解成 CSV 和 Python 框架之间的"翻译层"。

CaseRunner 负责用例级执行

读完配置后,真正执行用例的是 CaseRunner

它做的事情不是某一个具体动作,而是组织整条用例:

python 复制代码
steps_all = self.steps_by_case.get(case.case_id, [])
pre_steps, mid_steps = split_pre_session_steps(steps_all)
main_steps, post_steps = split_post_session_steps(mid_steps)

session = self.browser_manager.create_session(case, self.devices[case.device_id])
recorder = NetworkRecorder(session.page)
recorder.start()
runner = StepRunner(session, self.elements, recorder, self.variables)

for step in main_steps:
    detail = runner.run(step)

这段流程里有几个角色:

  • BrowserManager 创建浏览器会话。
  • NetworkRecorder 开始监听网络请求。
  • StepRunner 执行具体步骤。
  • CaseRunner 负责步骤顺序、失败处理和结果收集。

也就是说,CaseRunner 不关心 click 到底怎么点,它只关心"这一条用例有哪些步骤、按什么顺序执行、失败后怎么办"。

StepRunner 负责把 action 翻译成动作

真正把 action=click 翻译成 Playwright 操作的是 StepRunner.run()

看几个核心分支:

python 复制代码
def run(self, step: StepConfig) -> str:
    action = step.action

    if action == "goto":
        url = self.variables.resolve_text(step.value)
        self.page.goto(url, wait_until="domcontentloaded")
        return f"opened url: {url}"

    elif action == "click":
        loc = self.locator(step.target)
        loc.click(timeout=DEFAULT_TIMEOUT)
        return f"clicked element: {step.target}"

    elif action == "fill":
        loc = self.locator(step.target)
        text = self.variables.resolve_text(step.value)
        loc.fill(text)
        return f"filled element: {step.target} len={len(text)}"

这里的逻辑很直接:

  • goto 打开页面。
  • click 点击元素。
  • fill 输入文本。

但它们都不是直接使用 CSV 里的原始字符串。比如 fill 会先调用:

python 复制代码
self.variables.resolve_text(step.value)

所以 CSV 里可以写:

csv 复制代码
fill,channel_id_input,${channel_id}

运行时再把 ${channel_id} 替换成变量池里的真实值。

target 如何变成页面选择器

click 分支里有一行:

python 复制代码
loc = self.locator(step.target)

对应方法是:

python 复制代码
def locator(self, target: str):
    selector = self.elements.get(target, target)
    if not selector:
        raise AssertionError("Element target is empty")
    return self.page.locator(selector)

这段代码有一个小设计:如果 target 能在 elements.yaml 里找到,就使用配置里的选择器;如果找不到,就把 target 本身当成选择器。

所以两种写法都可以:

csv 复制代码
click,init_button

或者:

csv 复制代码
click,#initSDK

日常更推荐第一种。因为 init_button 是业务含义,#initSDK 是页面实现细节。页面选择器以后变了,只需要改 elements.yaml,不用到所有 CSV 里搜索替换。

wait_request 和 extract 也是关键字

关键字不只负责页面操作,也负责网络断言和变量提取。

比如登录接口:

csv 复制代码
GUEST_001,7,wait login api,wait_request,,/test-api/oauth2/login,status=200;response.code=200;response.data.access_token=not_empty,login_response,6,true

StepRunner 里对应的是:

python 复制代码
record = self.recorder.wait_for_url_contains(
    self.variables.resolve_text(step.value),
    timeout=DEFAULT_TIMEOUT,
    matcher=lambda item: assert_expectations(
        item,
        self.variables.resolve_text(step.expect),
    ),
)
if step.save_as:
    self.variables.set(step.save_as, record.to_dict())

这一步会等待 URL 包含 /test-api/oauth2/login 的请求,并检查:

text 复制代码
status=200
response.code=200
response.data.access_token=not_empty

如果匹配成功,还会把整条网络记录保存到变量 login_response

下一步就可以提取字段:

csv 复制代码
GUEST_001,8,extract guest name,extract,login_response,response.data.userName,,guest_a,7,true

对应代码:

python 复制代码
value = self.variables.extract(step.target, step.value)
self.variables.set(step.save_as, value)

这就是为什么 CSV 里可以写"等待接口 -> 保存响应 -> 提取 userName -> 后续断言"。

这套设计的边界

关键字驱动不是为了让 CSV 变成另一种编程语言。

它适合表达稳定、重复、可抽象的测试动作,比如:

  • 打开页面。
  • 点击按钮。
  • 输入文本。
  • 等待文案。
  • 等待接口。
  • 提取字段。
  • 断言变量。
  • 保存浏览器状态。

但如果某个流程包含大量复杂判断、循环、动态分支,就要考虑是不是应该沉淀成新的 Python 关键字,而不是在 CSV 里硬凑。

一个简单判断标准是:

text 复制代码
如果测试同学能用一句业务动作说清楚它,就适合做关键字。
如果必须解释很多代码细节,可能就不适合直接塞进 CSV。

小结

这一篇要记住一条主线:

text 复制代码
case_steps.csv
  -> csv_loader.py
  -> StepConfig
  -> test_csv_runner.py
  -> CaseRunner
  -> StepRunner.run()
  -> Playwright / NetworkRecorder / VariableStore

以后看到一行 CSV,不要只把它当成表格数据。

它最终会变成一个 StepConfig,交给 StepRunner 根据 action 执行。

理解了这条链路,后面再看用例依赖、变量池、网络断言和失败现场保存,就不会觉得它们是散的功能,而是同一套执行引擎上的不同能力。

相关推荐
创意岛1 小时前
AI时代,你的品牌在城市发展中“被消失”了吗?
人工智能·python
weixin_444012931 小时前
CSS如何实现单选按钮自定义样式_利用伪元素隐藏默认UI
jvm·数据库·python
X56611 小时前
CSS如何利用Grid重写老旧的表格布局
jvm·数据库·python
ㄟ留恋さ寂寞1 小时前
mysql如何配置MySQL的连接保持_调整tcp_keepalive设置
jvm·数据库·python
2301_783848651 小时前
Less如何构建CSS样式库_通过继承机制优化组件化开发
jvm·数据库·python
七夜zippoe1 小时前
OpenClaw Browser 自动化:表单填写实战
服务器·自动化·表单·browser·openclaw
tedcloud1237 小时前
UI-TARS-desktop部署教程:构建AI桌面自动化系统
服务器·前端·人工智能·ui·自动化·github
曦月逸霜9 小时前
啥是RAG 它能干什么?
人工智能·python·机器学习
2301_7693406710 小时前
如何在 Vuetify 中可靠捕获 Chip 关闭事件(包括键盘触发).txt
jvm·数据库·python