Playwright+PyTest中使用Fixture初始化页面对象时出现AttributeError的解决方案及最佳实践
嘿,我看了你遇到的问题,这个错误其实是因为对async fixture的使用方式理解有点偏差,咱们一步步来解决它,顺便聊聊这类场景的最佳实践。
问题根源
你看到的AttributeError: 'coroutine' object has no attribute 'url'和RuntimeWarning,核心原因是你在测试函数里对async fixture做了多余的await操作。
当你定义了async类型的fixture,pytest-asyncio会自动帮你处理异步执行的逻辑:在运行测试前,它会自动await这个fixture,所以测试函数参数里的login_page_fixture已经是fixture yield出来的LoginPage实例了,根本不是coroutine对象。你额外写的login_page = await login_page_fixture,相当于把一个普通的页面对象当成coroutine去等待,反而生成了一个无效的coroutine对象,自然找不到url属性。
快速修复
只需要修改你的测试函数,去掉多余的await即可:
@pytest.mark.asyncio async def test_login_with_valid_credentials(login_page_fixture): # 直接使用fixture参数,它已经是初始化好的LoginPage实例 print(f"login page url: {login_page_fixture.url}") # 后续可以直接调用页面对象的方法,比如跳转、登录 # await login_page_fixture.goto() # await login_page_fixture.login("Admin", "admin123")
进阶最佳实践
咱们再优化一下你的代码结构,让它更符合Playwright+PyTest的最佳实践:
1. 拆分通用fixture,提升复用性
把浏览器、页面、页面对象的fixture拆分开,这样其他测试用例也能复用通用的browser和page fixture:
import pytest from playwright.async_api import async_playwright from ...infra.page_objects.login.login_page import LoginPage # 浏览器fixture:负责启动和关闭浏览器 @pytest.fixture async def browser(): async with async_playwright() as playwright: # 可以加headless=False方便调试,上线后再改回True browser = await playwright.chromium.launch(headless=False) yield browser await browser.close() # 页面fixture:负责创建和关闭页面,依赖browser @pytest.fixture async def page(browser): page = await browser.new_page() yield page await page.close() # 登录页fixture:负责初始化页面对象,甚至提前完成页面跳转和校验 @pytest.fixture async def login_page(page): login_page = LoginPage(page) # 提前跳转到登录页并校验页面加载完成,测试函数直接用现成的页面对象 await login_page.goto() await login_page.verify_page_loaded() yield login_page
2. 修复页面对象里的小问题
你的LoginPage类中verify_page_loaded方法引用了self.login_title,但这个属性没有在__init__里定义,会导致新的报错,补充上对应的元素定位:
from playwright.async_api import Page, expect class LoginPage: def __init__(self, page: Page): self.page = page self.url = 'https://opensource-demo.orangehrmlive.com/web/index.php/auth/login' self.header = page.locator(".orangehrm-login-branding") self.header_logo = page.get_by_role("img", name="company-branding") self.username_field = page.get_by_placeholder("Username") self.password_field = page.get_by_placeholder("Password") self.login_button = page.get_by_role("button", name="Login") self.forgot_password_link = page.get_by_text("Forgot your password?") # 补充login_title的元素定位 self.login_title = page.locator("h5.orangehrm-login-title") async def goto(self): await self.page.goto(self.url) async def verify_page_loaded(self): await expect(self.header).to_be_visible() await expect(self.login_title).to_be_visible() # 其他方法保持不变...
3. 简化测试函数
现在测试函数可以非常简洁,直接使用初始化好的页面对象:
@pytest.mark.asyncio async def test_login_with_valid_credentials(login_page): print(f"login page url: {login_page.url}") # 执行登录操作 await login_page.login("Admin", "admin123") # 这里可以添加断言,比如验证登录后的页面元素 # 示例:await expect(login_page.page.locator(".oxd-userdropdown-name")).to_contain_text("Admin")
这样调整后,代码结构更清晰,fixture的复用性更高,测试逻辑也更简洁。
备注:内容来源于stack exchange,提问作者autimation_guy

