ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

pytest+Allure:构建业务可读的自动化测试报告与封装实战

pytest+Allure:构建业务可读的自动化测试报告与封装实战 这个系列走到第42篇终于轮到报告部分做一次“体面工程”。pytest、Selenium、PageObject、日志与基础工具类在前面都已经就位但如果你把当时的HTML报告直接丢给业务方或者测试负责人大概率会收到一连串追问这条用例叫什么对应哪个业务场景失败的时候页面长成什么样这其实暴露了一个很现实的问题——自动化测试的报告如果只是给写代码的人自己看那它只能叫日志不叫交付物。今天这篇就来解决这件事把allure报告里的显示信息补齐让每条用例带业务含义、带步骤、带截图同时把几个高频操作封装成工具减少后续用例代码里的重复劳动。这篇内容适合已经在跑pytestSelenium或者刚把框架搭起来、报告还是一堆函数名堆叠的团队。如果你正处在这个阶段那这篇文章基本是照着就能用的。1. 先想清楚报告到底是写给谁看的1.1 报告是自动化测试的最终交付物自动化测试代码写得再漂亮如果产出物不能被团队理解价值就打了折扣。测试报告在大多数团队的流转路径里最终会到达三类人执行用例的测试工程师、负责质量评估的leader、以及偶尔来看一眼的研发同事。这三类人里前两类不一定关心你用了什么选择器或者等待策略他们关心的是这轮回归覆盖了哪些功能点失败了哪些失败原因是什么影响范围有多大。所以当我开始重构这个系列的报告模块时优先级排在最前面的是“信息完整性”其次才是“样式好不好看”。allure能够成为这个系列最终选定的报告方案核心原因不是它界面清爽而是它对“结构化表达用例信息”的支持非常成熟feature、story、title、description、severity、attachment、step这些概念组合起来基本能覆盖一份优质测试报告的必备要素。1.2 换成allure之前我们在报告上吃了哪些亏最早这个系列用pytest自带的简单输出配合HTML插件跑完用例之后的问题集中在三块。第一报告里用例名默认是函数名比如test_login_with_correct_username_and_password这类长名字勉强能猜出意图但时间一久用例数量上百条列表里全是这种平铺的名字别说别人自己都要找半天。第二失败用例不携带上下文日志里有打印语句还能看看但页面状态完全不可见研发经常会反问“失败时页面卡在哪一步”。第三没有环境信息、没有执行时间分布、没有用例重要等级也就没有筛选维度。这套不足让我意识到报告不应该只被当作“测试结束后的附加产物”而应该当成框架的一部分来设计。接下来的落地方式就是两个方向一个是给allure注入更丰富的用例元信息另一个是完善截图、日志、等待这些基础封装让报告里的每个细节都有来源。2. 让报告开口说话allure基础显示信息的接入2.1 环境准备和两种生成报告的方式allure落到pytest里需要两个部分一个是allure-pytest插件负责把pytest执行过程中产生的用例信息转成JSON格式的中间结果另一个是allure命令行工具负责把这些中间结果生成可浏览的HTML报告。安装部分用pip完成。pip install allure-pytest然后需要下载allure命令行工具。这里的前提是机器上有Java运行环境allure是Java生态的工具一般1.8以上的JDK都能跑。下载zip包后解压把解压目录下的bin路径加到系统环境变量PATH里。验证安装可以执行allure --version能打印出版本号说明命令行工具可用。生成报告的方式我列出两类常用场景。本地调试阶段推荐先执行用例生成中间结果再直接起一个本地服务预览pytest --alluredir./allure-results allure serve ./allure-resultsserve命令会临时起一个Web服务并把报告打开到默认浏览器改完代码想快速看效果这个模式最高效。持续集成或需要归档报告的时候用generate生成静态文件allure generate ./allure-results -o ./allure-report --clean值得一提的细节是--clean参数。第二次执行generate时如果不带它allure会把历史生成的旧文件混在一起时间久了报告内容会非常混乱。我习惯把它当作默认参数写进团队的执行脚本里。还有个概念上的区分需要澄清allure-results是中间产物每次执行都会重新生成它不应该被提交到gitallure-report是最后给人看的静态站点如果要做报告归档保留这个目录才有意义。2.2 核心装饰器feature、story、title、descriptionallure的显示信息很大程度上是通过装饰器在源码层面注入的。最基础的四个装饰器是feature、story、title、description。import allure import pytest allure.feature(登录模块) allure.story(正常登录场景) allure.title(使用正确的账号和密码可以登录成功) allure.severity(allure.severity_level.CRITICAL) allure.description( 用例验证范围 1. 输入正确的用户名 2. 输入正确的密码 3. 点击登录按钮后跳转到首页 预期结果登录成功首页展示用户昵称。 ) class TestLogin: def test_login_success(self, driver): ...这几个装饰器的作用在报告页面上看得很清楚feature会作为报告里的“功能模块”分块展示一般对应业务模块比如登录、订单、支付story对应功能模块下的业务场景比如登录模块下的正常登录、密码错误、账号锁定title是报告里用例列表直接展示的标题通常是一条完整可读的业务描述而不是代码里的函数名description用来承载更详细的用例说明支持Markdown格式适合补充前置条件和预期结果。这里有一个从团队协作角度出发的层级设计建议feature对应需求模块story对应测试场景title对应具体用例。这样一份报告出来之后你可以按模块统计通过率也可以看到每个场景下用例的分布业务方和研发都不需要去猜“这个用例测的是什么”。2.3 severity分级和动态标签除了上面四个装饰器我日常使用频率比较高的还有severity和dynamic系列。severity用来标记用例的重要程度allure提供了blocker、critical、normal、minor、trivial五个级别。这个信息对回归结果评估很重要。一个回归周期结束如果只有normal级别的用例失败可以评估为风险较低如果CRITICAL或BLOCKER级别失败就需要立即停止上线流程。所以我的习惯是把核心链路用例标记为critical边界和异常类用例标记为normal以下这样报告一打开就可以按严重程度快速排序定位重点问题。allure.dynamic是另一个很实用的能力它允许在用例执行过程中动态修改显示信息。典型场景是数据驱动测试每一条数据对应一个执行子用例你可以在测试方法内部根据参数动态设置标题、story、甚至severity。import allure import pytest allure.feature(搜索模块) pytest.mark.parametrize(keyword, [python, 自动化测试, selenium]) def test_search(keyword): allure.dynamic.title(f搜索关键字{keyword}) allure.dynamic.description(f验证搜索框输入 {keyword} 后的结果展示)这样做的好处是参数化用例在报告里不再是一串索引号而是每一条都有可读的标题。上一轮执行了100条数据驱动用例想看某条关键字的执行情况直接在报告里搜索标题就能定位效率高很多。3. 失败要有凭有据截图采集与自动挂载3.1 截图为什么不能临时在代码里写很多人在第一次遇到失败用例时会写这么一句driver.get_screenshot_as_file(fail.png)执行完之后就后悔了因为下一次失败会把这张图覆盖掉而且文件名没有时间信息根本不知道是哪次执行留下的。更麻烦的是用例失败时往往已经抛了异常如果截图这句写在测试方法内部很可能根本执行不到。所以截图必须走到统一封装并且在pytest的失败钩子里自动完成而不是指望每个用例工程师在代码里手写。这个系列的做法是把截图采集放到一个独立的工具类里只负责两件事生成带时间戳的文件名把页面截图保存到固定目录。3.2 一个简单的截图处理器设计这类工具类时我会遵循一个原则方法要足够简单不掺入业务判断。# utils/screen_capture.py from datetime import datetime from pathlib import Path class ScreenCapture: def __init__(self, driver, save_dirscreenshots): self.driver driver self.save_dir save_dir Path(self.save_dir).mkdir(parentsTrue, exist_okTrue) def capture(self, name): timestamp datetime.now().strftime(%Y%m%d_%H%M%S) file_name f{name}_{timestamp}.png file_path Path(self.save_dir) / file_name try: self.driver.get_screenshot_as_file(str(file_path)) return file_path except Exception as exc: # 截图失败时不能影响原始用例结果判断 print(f[screen_capture] 截图失败: {exc}) return None这段逻辑里有几个容易被忽略的细节。目录必须预先创建否则get_screenshot_as_file会直接抛异常。文件名里带上时间戳可以避免多轮执行的截图互相覆盖而且后续排查问题时能根据时间对应到具体那一轮运行。截图本身失败时我用异常捕获兜住绝不能在失败用例的现场再因为截图动作本身搞出二次异常否则就检查不到真实问题了。3.3 在pytest钩子里自动挂载失败截图截图处理器有了还需要让它在用例失败时自动执行。这里用pytest的pytest_runtest_makereport钩子最合适它可以拿到用例执行报告判断执行阶段以及是否失败。# conftest.py import allure import pytest pytest.hookimpl(tryfirstTrue, hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.when call and report.failed: driver item.funcargs.get(driver) if driver: try: allure.attach( driver.get_screenshot_as_png(), name执行失败截图, attachment_typeallure.attachment_type.PNG ) except Exception: pass这段钩子里有几个容易踩的坑。item.funcargs.get(driver)是拿fixture实例的关键方式前提是这个用例函数确实声明了driver参数。如果有的用例没有走driver这里拿到的是None所以要先做空值判断。另一个被新手忽略的点是report.when call这个条件。pytest在setup、call、teardown三个阶段都会调用这个钩子如果只判断report.failed会在setup阶段失败时也尝试截图。对于大多数Web UI用例来说setup阶段连driver可能都没准备好硬截图只会增加无意义的报错日志。加上when条件判断后只有用例体执行阶段失败才触发截图更加精准。关于截图保存的位置我建议在配合allure时直接用allure.attach把图片二进制挂到报告里而不是只保存本地文件。这样所有和这条用例相关的信息全部集中在这份报告里交付起来一份HTML就够了不需要额外打包一堆png文件。4. 其他封装方法把高频动作沉淀成公共工具4.1 等待逻辑封装让用例代码不再重复写WebDriverWaitSelenium自动化里使用最多的“隐性技术债”是重复的显式等待代码。几乎每个页面操作都需要等待元素可见、可点击、存在。如果每个页面里都来一段WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, login)))累不累而且很多人对timeout的取值没有统一标准有的页面写5秒有的写15秒等到条件变了就到处改。我的做法是做一个统一的等待工具模块把高频等待条件收敛成几个方法。# utils/wait_util.py from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class WaitUtil: def __init__(self, driver): self.driver driver def wait_visible(self, locator, timeout10): return WebDriverWait(self.driver, timeout).until( EC.visibility_of_element_located(locator) ) def wait_clickable(self, locator, timeout10): return WebDriverWait(self.driver, timeout).until( EC.element_to_be_clickable(locator) ) def wait_presence(self, locator, timeout10): return WebDriverWait(self.driver, timeout).until( EC.presence_of_element_located(locator) )这里的locator统一使用元组格式比如(By.ID, username)。为什么不用字符串因为Selenium的expected_conditions接口接受的标准参数就是By类型的元组统一这个格式之后所有页面对象都能直接调用减少了一层转换。超时时间的默认值我建议按场景分层页面加载类等待给15秒元素交互类给10秒按钮可点击给5秒。这个值不是随手写的而是基于日常执行网络状况和页面平均响应时间来调整的。如果内部系统访问慢所有默认值统一提高比每个用例单独改要省事得多。4.2 日志上下文自动记录把运行日志写进allure步骤另一个高频需求是日志。平时用例跑挂了大家普遍习惯直接看报错堆栈但如果在allure的步骤里能看到每一步具体操作定位问题的速度会快非常多。allure提供了allure.step可以手动包裹步骤但如果每个用例里都手写with allure.step(...)写起来还是累。我采取的方案是在conftest里做一层pytest的hook把测试方法级别的日志和allure步骤桥接起来。做法是在pytest_runtest_setup和pytest_runtest_teardown阶段写一条进入和退出的日志在用例体内如果想要更细粒度的步骤可以结合logger在操作前后手动记录通过日志插件把关键行同步到allure的报告里。# conftest.py import logging import allure import pytest logger logging.getLogger(ui_auto) pytest.hookimpl(hookwrapperTrue) def pytest_runtest_call(item): with allure.step(f开始执行用例{item.name}): yield with allure.step(f用例执行完成{item.name}): pass这个封装解决的问题是当用例内部逻辑比较复杂比如登录后连续做了三步业务操作中间某一步出错时你能在报告里看到步骤边界而不是只有一段混合日志。对于后续接手维护用例的同事而言这种“看得见的执行轨迹”比几百行终端输出友好太多了。4.3 重试机制封装flaky用例不再直接判失败做Web UI自动化最让人烦躁的事情是同一份代码同一套环境昨天跑全绿今天跑红三条重跑一次又绿了。这类用例大概率是受页面加载时机、接口延迟、前端动画等因素影响的间歇性问题。如果每次失败都给人看很容易让团队对自动化报告失去信任。pytest生态里有一个插件专门处理这类问题pytest-rerunfailures。安装后可以给指定用例打标也可以在命令行统一配置重跑次数。pip install pytest-rerunfailures用法上命令行方式最省事pytest --reruns 2 --reruns-delay 1表示失败后重跑2次每次间隔1秒。也可以对单个用例做精细控制import pytest pytest.mark.flaky(reruns2, reruns_delay1) def test_search_result(): ...用重试的出发点是降低环境因素对报告准确性的干扰但也要设置上限。我的项目里统一规定最多重跑2次因为重试次数越多会拉长整体执行时间而且一个用例如果连续3次都失败大概率是真实缺陷不是环境抖动。这里有个版本兼容的坑值得特别提醒。老版本插件对新的pytest版本支持一直比较滞后我遇到过pytest升级到8.x之后pytest-rerunfailures直接报兼容性错误导致全部用例无法收集的情况。处理方式是锁定pytest版本和插件版本配套使用或者直接在requirements.txt里约束pytest-rerunfailures某一版本而不是只装不锁版本。4.4 环境信息注入封装让报告自带运行上下文除了用例本身的信息报告里还应主动暴露这次测试的运行环境浏览器类型、对应版本、被测系统地址、执行时间。这份信息平时不起眼一旦遇到跨环境的问题比如测试环境地址变了、Chrome升级后导致兼容问题它的作用就非常明显。allure支持通过environment.properties文件向报告注入环境信息。执行结束后在allure-results目录下放置这个文件即可。# conftest.py from pathlib import Path ALLURE_RESULTS_DIR allure-results def pytest_sessionfinish(session, exitstatus): env_file Path(ALLURE_RESULTS_DIR) / environment.properties env_file.parent.mkdir(parentsTrue, exist_okTrue) env_file.write_text( BrowserChrome\n Browser.Version120.0\n EnvTest\n BaseUrlhttps://example.com\n, encodingutf-8 )pytest_sessionfinish会在整个测试会话结束前触发此时写environment文件时机刚刚好。注意目录要先创建否则文件写不进去。除了环境信息allure还支持把需求ID、缺陷ID、用例链接关联进报告。比如你想让某条用例关联到Jira上的一个故事可以加装饰器allure.link(https://jira.example.com/browse/PROJ-101, name需求单) def test_login(): ...这在实际交付里非常有用特别是测试报告需要对照需求追溯的场景一点链接就能跳转到需求单省去来回翻文档的时间。5. 实战组合把封装串起来跑一遍5.1 完整的登录用例演示理论讲完把上述内容拼到一条用例里看看实际效果。# test_cases/test_login.py import allure import pytest from utils.wait_util import WaitUtil from pages.login_page import LoginPage allure.feature(登录模块) allure.story(正常登录场景) allure.title(使用正确的账号和密码登录成功) allure.severity(allure.severity_level.CRITICAL) def test_login_with_valid_credentials(driver): login_page LoginPage(driver) wait WaitUtil(driver) with allure.step(打开登录页面): login_page.open() with allure.step(输入用户名): wait.wait_visible(login_page.username_input).send_keys(tester01) with allure.step(输入密码): wait.wait_visible(login_page.password_input).send_keys(123456) with allure.step(点击登录按钮): wait.wait_clickable(login_page.login_btn).click() with allure.step(断言跳转后页面显示用户昵称): assert tester01 in login_page.get_user_nickname()这段代码里每一步操作都被allure.step包裹配上WaitUtil的显式等待。一旦某个步骤失败报告里可以定位到是哪一步操作出了问题这一步操作时的页面截图也会通过前面conftest里的失败钩子自动挂载。# pages/login_page.py from selenium.webdriver.common.by import By class LoginPage: def __init__(self, driver): self.driver driver self.username_input (By.ID, username) self.password_input (By.ID, password) self.login_btn (By.ID, loginBtn) self.user_nickname (By.CLASS_NAME, nickname) def open(self, urlhttps://example.com/login): self.driver.get(url) def get_user_nickname(self): return self.driver.find_element(*self.user_nickname).text5.2 报告效果和评审视角执行完这条用例打开allure报告你会看到左侧的“登录模块”分类下挂着一个“正常登录场景”点进去是三条用例如果多用例或者一条用例用例标题是“使用正确的账号和密码登录成功”而不是test_login_with_valid_credentials。再点进去用例描述、严重程度、操作步骤、失败截图全部按层级展示下面是测试执行的环境信息。这样的报告发给研发和业务方基本不需要额外解释“失败在哪一步”一句话就能对齐登录模块是好的订单模块有一条支付超时用例连续失败已经附了截图。我自己的经验是这样的报告在评审会上可以把沟通成本压缩掉至少一半因为看报告的人能够自己得出大部分结论。6. 常见问题排查与避坑台账最后整理一份自己在接入allure和封装过程中踩过的坑都来自实际执行照着排查能省不少时间。现象可能原因处理方式执行pytest后找不到allure命令allure命令行工具未安装或bin目录不在PATH重新检测Java环境把allure的bin路径加入PATH并重启终端报告中用例名称仍然显示函数名装饰器只在方法级生效可能放在类上但方法里未加title确认title装饰器直接挂在测试函数上失败用例没有截图item.funcargs.get(driver)取不到fixture确认用例参数声明了driver且driver是函数级fixture截图长时间黑屏或只有顶部内容页面尚未加载完成就执行截图截图前增加等待条件或把截图钩子的触发阶段独立封装重试后的用例覆盖了首次失败截图allure-attach和重试逻辑相互影响在makereport钩子里过滤重试标记只在最终失败时attach报告中中文乱码环境变量或JVM默认编码问题检查系统编码设置避免使用特殊符号作为用例标题allure-results目录越来越大中间结果未及时清理执行命令前加rm -rf ./allure-results或使用--clean参数pytest升级后rerunfailures不生效插件版本不兼容锁定pytest与pytest-rerunfailures的版本组合这里着重说下重试和截图之间的冲突问题。默认情况下用例重试前的失败也会触发pytest_runtest_makereport钩子所以首次失败截图会被收集到中间结果里。如果用例最终重跑通过报告里却还挂着失败截图看起来非常奇怪。我后续会在hook里判断是否存在重试标记只有最后一次尝试失败时才附加截图这个细节虽然小但对报告可信度的影响非常大。排查表格里还有一类高频问题就是allure环境信息没有出现在报告里。这通常不是代码问题而是environment.properties写入的时机和目录位置不对。allure只读取allure-results目录下的该文件如果你把文件写错到项目根目录报告里永远看不到环境信息。结尾之前再给一个小建议所有封装在上线前都要做一个“负面测试”也就是人为让用例失败一次确认报告里的截图、日志、步骤展示都符合预期。我自己就曾因为只测了全绿路径结果第二天真实失败时才发现截图钩子里有逻辑问题白白浪费了一整轮分析时间。每个新封装的模块都应该用一次“计划内的失败”来验证它是否真正可靠。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进