用例不是孤立执行的:依赖、变量池与 storage_state 设计

用例不是孤立执行的:依赖、变量池与 storage_state 设计

摘要

前一篇讲了关键字驱动:CSV 里的一行步骤,最后会被 StepRunner 翻译成浏览器操作、网络断言或变量处理。

这一篇继续讲一个更贴近业务的问题:游客登录怎么验证。

游客登录看起来只是点击一个按钮,但真正要验证的是设备状态和游客身份之间的关系:

text 复制代码
新设备第一次登录,应该创建新游客。
同一个设备再次登录,应该还是同一个游客。
不同设备登录,应该创建不同游客。

为了验证这个规则,框架需要三个能力:用例依赖、变量池和 storage_state

为什么游客登录不是一条孤立用例

如果只测"能不能游客登录成功",一条用例就够了。

但现在要测的是"同一个设备还是不是同一个游客"。这就意味着第二条用例必须依赖第一条用例。

当前 CSV 里有三条游客登录相关用例:

csv 复制代码
case_id,module,case_name,enabled,depends_on,device_id,state_mode,state_file,case_vars
GUEST_001,guest_login,new device creates guest,false,,device_a,new,device_a_state.json,channel_id=...
GUEST_002,guest_login,same device keeps guest,false,GUEST_001,device_a,load,device_a_state.json,channel_id=...
GUEST_003,guest_login,different device creates different guest,false,GUEST_001,device_b,new,device_b_state.json,channel_id=...

这三条用例的关系是:

text 复制代码
GUEST_001:device_a 新设备登录,拿到 guest_a,并保存状态。
GUEST_002:加载 device_a 的状态,再次登录,断言还是 guest_a。
GUEST_003:换成 device_b 新设备登录,断言不是 guest_a。

也就是说,这不是三条完全独立的用例,而是一组状态型业务链路。

depends_on 决定用例顺序

cases.csv 里的 depends_on 表示用例依赖。

例如:

csv 复制代码
GUEST_002,...,depends_on=GUEST_001,...

表示运行 GUEST_002 之前,必须先运行 GUEST_001

框架里负责处理这个关系的是 DependencyResolver

python 复制代码
class DependencyResolver:
    def resolve(self, requested_case_ids: list[str]) -> list[str]:
        ordered: list[str] = []
        visiting: set[str] = set()
        visited: set[str] = set()

        def visit(case_id: str) -> None:
            if case_id in visited:
                return
            if case_id in visiting:
                raise ValueError(f"Circular case dependency detected at {case_id}")

            visiting.add(case_id)
            for dependency in self.cases[case_id].depends_on:
                visit(dependency)
            visiting.remove(case_id)
            visited.add(case_id)
            ordered.append(case_id)

        for case_id in requested_case_ids:
            visit(case_id)
        return ordered

这段代码做的是依赖排序。

如果你只指定运行:

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

框架不会直接跑 GUEST_002,而是先算出:

text 复制代码
GUEST_001 -> GUEST_002

这样 GUEST_002 需要的前置状态和变量才会存在。

依赖失败时,后续用例会跳过

排序只是第一步。

真正执行时,test_csv_runner.py 还会检查依赖用例是否通过:

python 复制代码
failed_dependencies = [
    dependency
    for dependency in case.depends_on
    if completed_statuses.get(dependency) != "passed"
]

if failed_dependencies:
    result = CaseResult(
        case_id=case.case_id,
        case_name=case.case_name,
        status=SKIPPED,
        steps=[StepResult(...)]
    )
else:
    result = runner.run_case(case)

这意味着:

text 复制代码
如果 GUEST_001 失败,GUEST_002 不会继续跑。

这个设计很重要。

因为 GUEST_002 的目标是验证"同一个设备再次登录还是同一个游客"。如果第一次登录都失败了,后面继续跑只会制造更多误导性的失败。

变量池保存跨步骤数据

用例依赖解决的是"先跑谁、后跑谁"。

但光有顺序还不够。

GUEST_002 要比较第二次登录的游客名和第一次登录的游客名,就必须能拿到 GUEST_001 里提取出来的值。

这个能力由 VariableStore 提供:

python 复制代码
class VariableStore:
    def __init__(self):
        self._values: dict[str, Any] = {}

    def set(self, name: str, value: Any) -> None:
        self._values[name] = value

    def get(self, name: str) -> Any:
        if name not in self._values:
            raise KeyError(f"Variable not found: {name}")
        return self._values[name]

    def resolve_text(self, value: str) -> str:
        def replace(match):
            return str(self.get(match.group(1)))

        return re.sub(r"\$\{([^}]+)}", replace, value or "")

可以把它理解成一次测试运行中的公共变量池。

比如 GUEST_001 里有两步:

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
GUEST_001,8,extract guest name,extract,login_response,response.data.userName,,guest_a,7,true

第一步把登录接口响应保存为 login_response

第二步从 login_response.response.data.userName 提取游客名,保存为 guest_a

后面的用例就可以引用:

csv 复制代码
GUEST_002,9,assert same guest,assert_var,guest_a_again,,eq:${guest_a},,8,true

${guest_a} 会在运行时被替换成变量池里的真实值。

extract 和 assert_var 如何配合

提取字段靠 extract

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

其中:

text 复制代码
target = login_response
value = response.data.userName
save_as = guest_a

意思是:

text 复制代码
从 login_response 这个变量里,按 JSON 路径 response.data.userName 取值,保存成 guest_a。

断言变量靠 assert_var

python 复制代码
actual = self.variables.get(step.target)
expression = self.variables.resolve_text(step.expect)

if expression.startswith("eq:"):
    expected = expression.removeprefix("eq:")
    assert str(actual) == expected
elif expression.startswith("ne:"):
    expected = expression.removeprefix("ne:")
    assert str(actual) != expected

所以:

csv 复制代码
assert_var,guest_a_again,,eq:${guest_a}

表示:

text 复制代码
guest_a_again 必须等于 guest_a。

而:

csv 复制代码
assert_var,guest_b,,ne:${guest_a}

表示:

text 复制代码
guest_b 必须不等于 guest_a。

这样就能用 CSV 表达跨用例的数据比较。

storage_state 保存浏览器状态

变量池保存的是接口返回里的业务数据,比如 userName

但"老设备"还需要浏览器状态,比如 cookie、localStorage 等。

Playwright 提供了 storage_state,框架把它包装成 save_statestate_mode=load

GUEST_001 结束前保存状态:

csv 复制代码
GUEST_001,11,save device state,save_state,,device_a_state.json,,,10,true

对应代码:

python 复制代码
def save_state(self, state_file: str) -> Path:
    path = STATES_DIR / Path(state_file).name
    self.session.context.storage_state(path=str(path))
    return path

GUEST_002 再加载同一个状态文件:

csv 复制代码
GUEST_002,...,device_a,load,device_a_state.json,...

BrowserManager.create_session() 里会处理:

python 复制代码
if case.state_mode == "load":
    if state_path is None or not state_path.exists():
        raise FileNotFoundError(...)
    options["storage_state"] = str(state_path)

context = browser.new_context(**options)

这样 GUEST_002 创建浏览器上下文时,就带上了 GUEST_001 保存的状态。

测试意义上,它就不再是一个全新设备,而是同一个设备的再次访问。

case_vars 让用例带入业务参数

cases.csv 里还有一个字段:

csv 复制代码
case_vars

例如:

text 复制代码
channel_id=83b8f361234e171cb7ea90d0228d7151

CaseRunner 在用例开始时会把它放进变量池:

python 复制代码
def _apply_case_vars(self, case: CaseConfig) -> None:
    raw = (case.case_vars or "").strip()
    if not raw:
        return
    for part in raw.split(";"):
        key, _, val = part.partition("=")
        self.variables.set(key.strip(), val.strip())

所以步骤里可以写:

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

这样同一套步骤可以复用,不同用例只要在 case_vars 里换参数。

排错时应该看哪里

如果依赖或变量相关用例失败,可以按这个顺序排查:

text 复制代码
1. cases.csv 里的 depends_on 是否写对。
2. 被依赖用例是否真的执行并通过。
3. extract 的 source 变量名是否和上一步 save_as 一致。
4. JSON 路径是否存在,比如 response.data.userName。
5. assert_var 里的 eq/ne 是否符合预期。
6. state_file 是否保存成功,state_mode=load 时文件是否存在。

最常见的问题是变量名写错。

比如上一步保存的是:

csv 复制代码
save_as=login_response

下一步却写:

csv 复制代码
target=login_resp

这时 VariableStore.get() 会直接报:

text 复制代码
Variable not found

这种错误不需要从 Playwright 查起,先查 CSV 字段最有效。

小结

这一篇要记住三件事:

text 复制代码
depends_on      -> 解决用例执行顺序
VariableStore   -> 解决跨步骤、跨用例的数据传递
storage_state   -> 解决浏览器状态复用

游客登录这种状态型场景,不是一条孤立用例能讲清楚的。

框架把"先跑哪条用例""保存哪个变量""复用哪个设备状态"都放到了 CSV 和执行器里。测试同学维护用例时,重点不是改 Python,而是把业务关系描述清楚。

相关推荐
2303_821287381 小时前
如何安装Oracle 12c Cloud Control_OMS服务端组件与Agent部署
jvm·数据库·python
m0_609160491 小时前
React Flow 边缘错位与消失问题的根源分析与 Hooks 重构方案
jvm·数据库·python
Marvel__Dead1 小时前
微调 Gemma 4 识别腾讯天御全系列验证码【解决方案-一个模型识别 滑块|文字点选|图标点选|空间点选】
人工智能·爬虫·python·验证码识别·ai 大模型
Agent手记1 小时前
成品发货全流程自动化,落地实操与错发漏发规避方案 | 2026企业级Agent端到端落地指南
运维·人工智能·ai·自动化
weixin_444012931 小时前
CSS怎样调整弹性项目排列顺序_使用order属性轻松控制DOM显示顺序
jvm·数据库·python
iuvtsrt1 小时前
SQL处理分组聚合时的NULL值处理_利用NVL函数
jvm·数据库·python
dinglu1030DL1 小时前
CSS如何利用Flex实现悬浮的侧边按钮组_利用fixed定位与flex布局组合
jvm·数据库·python
Pkmer1 小时前
Javthon古法: Python中哪些让人搞不清的参数
python·ai编程
Jetev1 小时前
如何利用SQL子查询进行非结构化数据处理_文本匹配
jvm·数据库·python