conftest.py 里明明写了 fixture,测试跑起来就是不用它。
不是报错,是静悄悄地不用——页面还是没登录的那个,浏览器还是上一个用例关掉的那个,截图该出的地方一张都没有。你去改 fixture,改了三遍,还是没反应。
我手上这套 Playwright + Pytest 框架跑了大半年,这种"写了却不生效"的现场,最后归起来只有 5 类。每一类都不难修,难的是判断你遇上的是哪一类——因为它们的表现看起来一模一样:fixture 好像没跑。
下面 5 个现场,代码都来自真实项目里那个 conftest.py。你可以对照自己的看一眼。
现场 1:签名里没请求它
先说最常见的一类,也是最冤枉的一类。
conftest 里有这个:
python @pytest.fixture(scope="function")
deflogged_in_page(browser):
"""已登录页面 - 自动登录"""
page = browser.new_page()
page.goto(SAUCE_DEMO_URL)
page.fill("#user-name", TEST_USERNAME)
page.fill("#password", TEST_PASSWORD)
page.click("#login-button")
page.wait_for_selector(".inventory_list")
yield page
page.close()
然后用例这么写:
python deftest_successful_login(self, page):
login_page = LoginPage(page)
login_page.load()
login_page.login() # 手动登一遍
assert login_page.is_login_successful()
fixture 没坏,是没人叫它。 你写了 logged_in_page,用例请求的是 page——page 是被请求了,logged_in_page 从头到尾没被谁提起过,pytest 不会主动执行一个没人要的 fixture。
于是每个用例都老老实实手动登录一遍。五十个用例登五十次,跑一轮多花三分钟。你以为是"框架慢",其实是自动登录写了没人用。
一行修法:
python deftest_successful_login(self, logged_in_page): # page → logged_in_page
不想改签名也行,挂标记:
python @pytest.mark.usefixtures("logged_in_page")
deftest_successful_login(self):
怎么提前发现:pytest --fixtures 会列出所有可用 fixture 和它们的定义文件。写完 fixture 先跑一次,看它有没有出现在列表里——在列表里但签名没写,就是这一类。
现场 2:同名覆盖,你改的是父级那份
这类最气人:你确认过代码没错,因为执行的根本不是你改的那个文件。
pytest 查找 fixture 的顺序是就近优先:用例所在目录的 conftest.py 会覆盖上级目录的同名 fixture。根目录一个 page,tests/cart/conftest.py 里又定义了一个 page——跑 cart 目录下的用例时,用的是子目录那个,你改根目录改到天亮也不会生效。
这个坑有个很典型的触发场景:为了让某个模块"特殊处理一下",有人在子目录里复制了一份 fixture 改了两行。半年后所有人都忘了它的存在。
一行修法:
bash pytest --fixtures | grep -A1 "^page"
输出里会写这个 fixture 定义在哪个文件。看到不是你改的那个路径,就找到了。
顺手的规矩:全项目同名 fixture 只允许有一份。要特殊化就换名字(page_mobile、admin_page),不要同名覆盖。
现场 3:scope 写错,第二个用例拿到已关闭的页面
fixture 能跑、也没被覆盖,但一跑到第二个用例就炸:
Target page, contextor browser has been closed这句报错几乎只有一个原因:页面级 fixture 被写成了 scope="session"。
python @pytest.fixture(scope="session") # ← 错
defpage(browser):
page = browser.new_page()
yield page
page.close()
session 级 fixture 整个测试会话只执行一次。第一个用例结束时 page.close() 已经执行了,第二个用例拿到的就是那个已经关掉的 page。而 page.close() 之所以执行,恰恰是因为 fixture 是正常的——这不是 fixture 没生效,是它太早收工了。
正确的分层是这样:
playwrightbrowser | session | |
contextpage | function | |
functionstorage_state 复用) |
一行修法:
python @pytest.fixture(scope="function") # session → function
想让登录只做一次、又不想共享页面,正确做法是存 storage_state,不是把 page 提升到 session。
现场 4:return 写成了 yield 的替代品
python @pytest.fixture
defpage(browser):
page = browser.new_page()
return page # ← 这一行
fixture 能跑,用例也过,看起来完全正常。问题是 return 之后的代码永远不会执行——如果你在 return 后面写了 page.close(),它是死代码。
后果不会立刻显形,是慢慢攒的:每次跑完整套用例,进程里多挂几个浏览器实例。跑十轮之后机器开始卡,你以为是 Playwright 吃内存,其实是你自己没关的那些窗口。
一行修法:
python yield page # return → yield
page.close() # teardown 必须写在 yield 之后
判断标准很简单:fixture 里需要收尾的(关页面、关连接、删临时文件、回滚数据),一律 yield;纯只读的、不需要收尾的(读配置、算个值),才用 return。
现场 5:间接请求断了——hook 里取不到 funcargs
这一类最隐蔽,因为它不影响用例本身,只影响你以为已经在跑的东西。
框架里有个失败自动截图的 hook:
python @pytest.hookimpl(hookwrapper=True)
defpytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == "call"and report.failed:
if"page"in item.funcargs:
take_screenshot(item.funcargs["page"], f"failure_{item.name}")
elif"logged_in_page"in item.funcargs:
take_screenshot(item.funcargs["logged_in_page"], f"failure_{item.name}")
看着没问题。但注意 logged_in_page 是自己 new_page() 的,它内部没有请求 page fixture——所以用 logged_in_page 的用例,item.funcargs 里没有 "page" 这个键。
一旦 elif 分支漏了一个 fixture 名字(比如新加了 admin_page),那个用例失败时就是静默不截图。CI 上一片绿,只有你去翻报告才发现:失败用例的截图目录是空的。
一行修法:
python page = next((item.funcargs[k] for k in ("page", "logged_in_page", "admin_page")
if k in item.funcargs), None)
if page isnotNone:
take_screenshot(page, f"failure_{item.name}")
不用再维护 if/elif 链,加新 fixture 只改一个元组。
怎么提前发现:故意让一个用例失败,看截图目录里有没有文件。测试失败自动截图这件事,本身也得被测试一次。
一次排查完,三条命令
遇上"fixture 没生效",按这个顺序跑,别猜:
bash # 1. 这个 fixture 到底在哪定义的?有没有被同名覆盖?
pytest --fixtures | grep -A1 "<fixture名>"
# 2. 它到底有没有被执行?执行了几次?
pytest --setup-show tests/test_login.py
# 3. 用例实际请求了哪些 fixture?
pytest -q --collect-only
--setup-show 是这三条里最有用的一个:它把每个用例的 SETUP/TEARDOWN 逐行打出来。你以为在跑的 fixture 没出现在 SETUP 里,就是现场 1 或现场 2;出现了但只出现一次,就是现场 3。
最后说一句
上面 5 个现场,我在同一个 conftest.py 里见过 3 个——不是三个不同的人写的,是同一个人在不同时间写的。fixture 的问题几乎都不是"不会写",是"写了没人验证它真的在跑"。
一个能跑的 fixture 和一次能跑的测试,中间隔着的就是这一层:你得让 fixture 自己也被检查一次。
我把这套框架里除错的顺序、5 类现场的判断表、还有那三条排查命令整理成了一页纸,照着走能省掉大部分试错。
本篇能拿走的:5 类现场对照清单(含每类的报错特征 + 判断命令)→ 公众号后台回复「fixture」,自动发你。
想要能直接跑的:这套 Playwright + Pytest 框架(page / logged_in_page / 失败自动截图 / HTML 报告全都在)已经放在星球,33 套源码包里它是下载率最高的一套 → 公众号后台回复「星球」看入口。
不承诺效果,只给路径:每套框架都要按你自己的项目改,改不动的地方后台私信我,说清你的场景,看到会回。