很多人学UI自动化看了一堆教程跟着demo敲了一遍又一遍但真正到了公司项目元素定位不稳、用例一多就乱、报告丑得没法跟领导汇报瞬间打回原形。我自己的经验是UI自动化必须在一个真实业务项目上完整跑一遍才能把“会写脚本”变成“会做项目”。这一篇就是接着TPshop商城项目实战系列往下写重点解决测试报告用Allure把用例结果变成老板看得懂、开发爱看、自己排查不费劲的漂亮报告。TPshop是一套开源的B2C商城系统页面覆盖登录、搜索、购物车、下单、支付等电商核心链路UI元素稳定非常适合做UI自动化学习。配合Pytest Selenium Allure这套组合基本是行业里最常见、招人简历上写得最多的技术栈。这篇我会从项目初始化、PO模式封装到Allure报告集成、报告美化、常见坑排查一条线讲完。无论你是刚转测试开发的新人还是想把手头项目测试报告升级的从业者都能直接照着抄。1. 项目实战概览为什么拿TPshop练手1.1 TPshop项目特点与自动化适配性TPshop是一个基于ThinkPHP开发的电商平台仿京东/天猫风格前后台分离不算彻底但页面结构对自动化非常友好。和那些纯前端SPA相比TPshop的URL路径直接反映了页面功能比如index.php?mHomecUseralogin就是登录页cCart就是购物车一眼就能看出用例对应哪个模块。再加上这些页面用了大量稳定的class和id不像某些项目全是动态随机id定位元素时不用费劲写XPath对刚入门的人特别友好。更关键的是TPshop覆盖了完整的电商主流程注册、登录、搜索、商品详情、加入购物车、购物车编辑、结算、订单提交。这些流程在真实公司项目里几乎一模一样做完这套自动化你去了新公司接触电商业务会非常顺。而且TPshop能本地部署不会动不动改版用例跑挂的概率远低于生产环境特别适合用来沉淀一套完整的自动化测试框架。我在这个系列里已经用TPshop讲过WebDriver基础操作和PO模式的雏形这一篇则聚焦在把报告这一环补齐。毕竟自动化项目不是“跑完就算”报告才是展示成果、定位问题、衡量回归价值的关键。Allure作为当前最流行的测试报告工具能把用例步骤、截图、日志、历史趋势都整合到一个网页里比pytest默认的终端输出和html插件好看太多。1.2 实战范围与预期效果本次实战我规划了四条主流程用例用户登录、商品搜索、加入购物车、购物车结算。这四条流程足够覆盖PO模式的核心设计思路又不会把文章写成一本百科全书。登录会暴露定位和等待问题搜索会涉及参数传递和断言购物车和结算则能验证多页面间的数据流转。把这些用例用Allure组织起来最终你会得到一个分类清晰、步骤完整、失败带截图的web报告。效果上我会演示三件事第一用allure.feature和allure.story把用例按业务模块分层报告左侧出现“导购”式目录第二用allure.attach在断言失败时自动截图报告里能直接看到当时页面状态第三用allure.environment和环境信息配置让报告显示测试地址、浏览器版本、执行时间等元数据。看到这套效果你自然明白为什么现在大厂测试团队都在用Allure。2. 环境搭建与项目初始化2.1 工具选型与版本组合工具版本这块很多新人喜欢一股脑装最新版结果遇到一堆兼容性问题。我自己目前在用的组合是Python 3.10.x Pytest 7.x Selenium 4.x Allure 2.24 allure-pytest 2.13Windows和macOS跑都没问题。Python 3.10在类型注解和语法上比3.7更舒服Selenium 4自带相对定位器和改进的等待API写起来比3.x简洁。Pytest 7对fixture和钩子的支持很好反正稳定版优先不要追求每个库都是最新。Allure命令行工具是生成报告的关键。在macOS上可以用brew install allureWindows可以用Scoop或直接下载zip包解压后加入PATH。安装完成后命令行输入allure --version能输出版本号就算成功。这里有个容易坑的点allure-pytest只是pytest和Allure之间的桥它把测试结果写成json和附件真正生成网页版报告还得靠Allure命令行两个缺一不可。如果你所在的团队已经有docker环境也可以用allure官方镜像但本地调试时我建议直接在宿主机装因为allure命令行的generate和serve交互太频繁每次进容器敲命令会很烦躁。等跑通了再考虑在Jenkins或GitLab CI里用工具链去集成。2.2 Allure安装与pytest集成配置先确认Python项目里装好了依赖requirements.txt大致长这样pytest7.4.0 selenium4.15.0 allure-pytest2.13.0 webdriver-manager4.0.0webdriver-manager这个库强烈建议装上它能自动下载和匹配浏览器驱动版本省去手工折腾chromedriver的麻烦。装完依赖后在项目根目录新建一个pytest.ini内容如下[pytest] addopts -s -q --alluredirallure-results testpaths testcases这里的关键是--alluredirallure-results它告诉pytest把Allure原始数据写到allure-results目录。-q是减少终端输出-s是让print能直接显示调试时有用。实际项目里你可以在CI中覆盖这些参数但本地开发时这样默认刚刚好。有个细节我得提醒allure-results目录会在每次运行时不断往里面塞新的json和附件如果不清理报告会堆积大量历史残留数据。所以本地跑用例之前最好手动删掉这个目录或者用pytest的--clean-alluredir参数。这个我在后面第4章会专门说。2.3 工程目录结构与pytest配置我习惯把项目结构分成这样tpshop_ui/ ├── config/ # 全局配置域名、超时时间、账号信息 │ └── settings.py ├── data/ # 测试数据客户可以放yml/json ├── pages/ # Page Object页面对象 │ ├── base_page.py │ ├── login_page.py │ ├── search_page.py │ └── cart_page.py ├── testcases/ # pytest用例 │ ├── conftest.py │ ├── test_login.py │ ├── test_search.py │ └── test_cart.py ├── utils/ # 工具类截图、日志、公共方法 │ ├── screenshot.py │ └── log.py ├── allure-results/ # allure原始结果 ├── allure-report/ # 生成的html报告 └── pytest.ini这个结构不是拍脑袋定的。pages目录把每个页面的元素定位和页面行为封装成一个类测试用例只关心业务动作不关心元素细节config目录集中管理环境配置切换测试环境只需改一个文件testcases里conftest负责初始化浏览器和全局fixture用例文件按模块拆分。后续接手的人哪怕没写过自动化也能按目录找到对应位置。conftest.py里我会定义一个session级的浏览器fixtureimport pytest from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.service import Service pytest.fixture(scopeclass) def browser(): options webdriver.ChromeOptions() options.add_argument(--window-size1920,1080) options.add_argument(--disable-gpu) driver webdriver.Chrome( serviceService(ChromeDriverManager().install()), optionsoptions ) driver.implicitly_wait(5) yield driver driver.quit()这里用scopeclass让同一个测试类里的用例共用浏览器速度快很多如果多个用例必须互相独立再改成scopefunction。隐式等待设成5秒是底线实际页面上我还要配合显式等待后面会说。3. PO模式封装与用例编写3.1 为什么要写PO而不是直接定位元素不少新手喜欢在用例里这么写driver.find_element(By.NAME, username).send_keys(admin) driver.find_element(By.NAME, password).send_keys(123456) driver.find_element(By.XPATH, //button[contains(text(),登录)]).click()这种写法看单个用例很爽但一旦页面改版比如用户名输入框的name从username改成user_name你得在几十条用例里搜索替换漏一个就等着报错。PO模式的本质是把页面元素和操作封装在一个类里用例只跟这个类打交道。页面改了只需要改对应的Page类用例代码不动维护成本直线下降。用一个生活化的类比你要给朋友带咖啡每次都跑进咖啡店跟店员说“来一杯拿铁少糖要热的”听上去没问题但每次都要重复。如果你把这段需求封装成一个“点拿铁”函数那朋友只需要说“照旧”具体操作你来做。PO就是这个“照旧”把页面细节藏起来让用例只说业务。PO模式还有一个隐藏优点它让测试代码的意图变清晰。比如login_page.login(admin, 123456)谁看了都知道这是在执行登录比看到一串find_element和click更直观。这也是为什么PO模式在面试里几乎必问在你自己的项目里也是必须具备的基本功。3.2 BasePage基础封装BasePage是所有页面类的基础我一般放四类东西元素定位的封装、点击输入操作、显式等待、截图。定位封装的关键在于让所有find操作都带显式等待。网上很多教程直接把Selenium自带的find_element用了个遍但遇到元素加载慢就偶发抖动实际上就是没用显式等待。这里我给出一版比较常用的BasePage核心代码from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By class BasePage: def __init__(self, driver, explicit_wait10): self.driver driver self.wait WebDriverWait(driver, explicit_wait) def find_element(self, locator): return self.wait.until(EC.visibility_of_element_located(locator)) def click(self, locator): self.find_element(locator).click() def input(self, locator, text): ele self.find_element(locator) ele.clear() ele.send_keys(text) def get_text(self, locator): return self.find_element(locator).text def screenshot(self, filename): self.driver.save_screenshot(filename)这个类虽然短但已经把大多数页面操作的公共部分收敛了。之后每个具体页面类继承它只需要传入自己的元素定位元组比如(name, username)代码就很清爽。我踩过的一个小坑是EC.visibility_of_element_located和presence_of_element_located是有区别的。前者要求元素可见后者只要求出现在DOM中。如果页面有弹窗遮罩元素在DOM里存在但被遮挡visibility会超时presence却直接返回。大部分按钮和输入框我推荐用visibility因为它更接近“用户真实可操作”的状态。3.3 登录、搜索、购物车页面对象编写以登录页为例TPshop登录页通常会需要用户名、密码、验证码。自动化处理验证码是个经典话题我这里用的是最简单的方案如果TPshop配置里有测试开关就关闭验证码否则用跳过验证码的后台账号或者先把cookie写进去。真正企业项目里一般会有万能验证码或白名单机制那是后话。页面类如下from pages.base_page import BasePage from selenium.webdriver.common.by import By class LoginPage(BasePage): username_loc (By.NAME, username) password_loc (By.NAME, password) submit_loc (By.XPATH, //button[contains(text(),登录)]) welcome_loc (By.XPATH, //a[contains(text(),会员中心)]) def login(self, username, password): self.input(self.username_loc, username) self.input(self.password_loc, password) self.click(self.submit_loc) def login_success_text(self): return self.get_text(self.welcome_loc)SearchPage则更简单搜索框、搜索按钮、搜索结果的第一个商品名from pages.base_page import BasePage from selenium.webdriver.common.by import By class SearchPage(BasePage): keyword_loc (By.NAME, keyword) search_btn_loc (By.XPATH, //button[classbtn-search]) first_goods_loc (By.XPATH, //div[classshop-list]/li[1]//a) def search(self, keyword): self.input(self.keyword_loc, keyword) self.click(self.search_btn_loc) def get_first_goods_name(self): return self.get_text(self.first_goods_loc)CartPage可以拆成添加商品和结算两步。添加商品必须从详情页点“加入购物车”然后跳转到购物车列表。结算时要勾选商品、点击“去结算”、进入确认订单页。这些流程每个公司细节不同但思路一致把“过程”封装成方法用例调用起来就像写自然语言一样。3.4 用例编写与Allure装饰器应用页面类写好后用例写起来就非常舒服了。一个登录用例可以这样import allure import pytest from pages.login_page import LoginPage from config.settings import BASE_URL allure.feature(用户管理) allure.story(登录) class TestLogin: allure.title(正确用户名密码登录成功) allure.severity(allure.severity_level.BLOCKER) def test_login_success(self, browser): login_page LoginPage(browser) login_page.open(BASE_URL /index.php?mHomecUseralogin) login_page.login(tpshop, 123456) assert 会员中心 in login_page.login_success_text()这里allure.feature相当于业务模块allure.story是子功能allure.title可以让报告里的用例标题变成中文而不是默认的函数名。allure.severity标记重要程度之后还能在Allure报告里按严重级别筛选用例适合每天回归时先跑冒烟级别。如果你想让报告步骤更细就用allure.step装饰拆分动作或者直接在代码里用with allure.step(输入用户名):包住关键动作。这样报告里就有树状步骤开发同学看到哪一步挂了不用再对着代码猜流程。后面章节我会专门讲这些装饰器能给报告带来什么效果。4. Allure报告深度配置与优化4.1 基础报告生成命令用例写完后运行命令有两种姿势。第一种是先跑用例生成原始数据pytest testcases/ --alluredirallure-results然后另开终端生成并打开报告allure generate allure-results -o allure-report --clean allure open allure-report第二种是直接一把梭跑完自动打开浏览器看报告pytest testcases/ --alluredirallure-results allure serve allure-resultsallure serve是平时调试推荐的方式它不会生成额外的静态文件直接起一个临时HTTP服务浏览器里看关掉就没了不污染项目目录。等到需要把报告留档发给别人或者要在CI里作为构建产物才用allure generate生成一个独立的目录。这里很多人会犯的错是src路径搞混。allure generate后面的第一个参数必须是存放Allure结果json的目录不是项目根目录也不是用例目录。你如果在根目录直接执行allure generate .它会提示找不到结果文件。另外--clean参数表示生成前清空目标目录避免旧报告残留我是建议每次都加除非你有意保留历史。4.2 让报告更“可读”动态更新用例描述固定的装饰器发虽然好用但用例多了以后你会在每个用例上面堆一大堆装饰器看起来非常臃肿。Allure提供了动态API可以在用例执行过程中动态设置标题、描述、链接尤其适合参数化用例或者需要把运行时数据放进报告的场景。最简单的例子allure.title(搜索商品-{keyword}) def test_search(self, browser, keyword手机): ...但如果你想在用例内部根据结果动态设置标题可以这样import allure def test_login_failed(self, browser): allure.dynamic.feature(登录模块) allure.dynamic.story(异常场景) allure.dynamic.title(登录失败时提示错误信息) allure.dynamic.description(验证密码错误不通过并检查提示文本)动态API非常适合数据驱动场景。比如拉了一堆账号执行登录报告里每条用例的标题可以动态拼上账号名不然后台看起来全是重复的“登录测试”根本分不清哪条是哪个账号。我还喜欢用allure.dynamic.link把用例关联到缺陷管理系统的ID。比如线上bug编号是BUG-1024在报告里点击即可跳到缺陷详情。这个动作在团队协作时特别有价值开发看报告发现失败一查关联的bug单上下文瞬间就补齐了。4.3 截图和日志附着失败现场重现UI自动化最痛苦的事情就是用例失败但抓不到现场。ElementNotInteractableException发生了什么页面上是不是弹了个遮挡层光靠终端报错信息基本猜不出来。所以我的原则是失败必须截图而且截图必须进报告。在conftest.py里可以加一个钩子用例失败后自动截图并附着到Allure报告import allure import pytest from utils.screenshot import capture_screenshot pytest.hookimpl(hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.when call and report.failed: if browser in item.fixturenames: driver item.funcargs[browser] capture_screenshot(driver, failure)具体截图函数我一般把PNG读成字节用allure.attach塞进去避免在报告展示时找不到本地文件路径import allure def capture_screenshot(driver, name): png driver.get_screenshot_as_png() allure.attach(png, namename, attachment_typeallure.attachment_type.PNG)也可以顺带把页面HTML抓下来便于在报告里直接审查DOM结构html driver.page_source allure.attach(html, namepage_source, attachment_typeallure.attachment_type.HTML)日志也同理用logging模块记录关键步骤在失败时把日志内容attch为TEXT。这样一份报告里既能看到步骤又能看到截图还能看日志开发不用再去测试那边翻聊天记录。4.4 历史趋势与清理策略Allure报告最有意思的一点是能展示测试执行的历史趋势包括用例总数、失败率、耗时趋势。但这个功能有个前提——必须保留上一份报告生成的history目录里的数据。很多人发现自己的报告里“History”页签为空就是因为上一份报告已经被--clean删掉或者根本没生成历史数据。正确的做法是第一次生成报告后allure generate --clean会得到一个新的allure-report目录目录下会有history文件夹。下一次跑用例前把上一次报告目录里的history文件夹整体拷贝到新的allure-results目录下然后再执行pytest --alluredirallure-results最后重新generate历史趋势就延续下来了。在本地调试时我没那么讲究通常直接删除allure-results目录重跑不关心历史。但如果是在Jenkins里保存构建产物就需要用一条命令来完成历史清理和拷贝。我给个参考脚本rm -rf allure-results cp -r allure-report/history allure-results/history pytest testcases/ --alluredirallure-results --clean-alluredir allure generate allure-results -o allure-report --clean注意最后一行执行后新的allure-report里又生成了一份新的history供下一次使用。这个循环在CI里是常规操作但确实容易让人掉坑我专门写出来就是不希望你到这一步卡住。5. 常见问题与排查实录5.1 报告里没有任何数据刚接触Allure时最容易遇到的问题pytest跑了一堆用例命令也执行了但allure open打开后报告是空的显示“没有测试数据”。这通常是allure-results目录下没有生成任何json导致的。先检查pytest是否装了allure-pytest。如果只装了allure命令行没有装allure-pytest那--alluredir参数 pytest根本不会认识当然也不会生成结果文件。其次是注意命令行顺序必须写pytest testcases --alluredirallure-results不能把--alluredir写在pytest之前shell会把参数解析错误。还有一种情况用例收集数量为0。比如testcases目录下没有以test_开头的文件或者pytest.ini里的testpaths指错了目录。用pytest --collect-only先检查一下收集数量如果显示0 collected那后面肯定啥也生成不了跟Allure半毛钱关系没有。5.2 用例中文乱码与编码问题我早期在Windows下跑TPshop项目Allure报告里中文全变成乱码用例标题是“注册”看得人脑壳疼。原因有两个方向。第一源码文件没有指定编码Python解释器用系统默认编码读文件中文就乱了第二生成的json文件被以非utf-8方式读取。解决方案很简单在pytest.ini或工程入口处加上编码声明并且保证所有.py文件保存为UTF-8 without BOM格式。如果用的是Windows自带的记事本保存时很容易带BOM这在Python 3下有些微妙问题建议直接用VS Code或PyCharm默认UTF-8就没事。另外如果是在Jenkins这类CI平台上还需要检查构建环境的默认字符集最好在启动命令前加上export PYTHONUTF81强制Python以UTF-8模式运行。这一条能解决80%的中文乱码问题。5.3 失败没截图排查全靠猜就算我们在conftest里写了失败自动截图还是有同学跟我说报告里看不到图。仔细一看截图函数里的driver参数根本不是同一个实例。比如用了多个fixture有的case拿的是browser有的case拿的是webdriverhook里只取了browser那其他fixture用例失败自然没有图。排查思路很简单在hook里打印出item.fixturenames看看失败用例的fixture名是什么。如果发现用例用的是不同名字的fixture要么统一命名为browser要么hook里做一个兼容判断拿到任一可用的driver。我后来干脆把driver封装成一个driverfixture所有用例都叫它就不会再有这种问题了。还有一个容易被忽略的坑pytest_runtest_makereport里只有when call阶段才适合判断失败。如果setup阶段就挂了比如浏览器压根没启动那driver不存在截图也没法截。此时我会判断一下try/except至少把异常信息attach进去不至于空手而归。5.4 用例重试与Allure状态合并回归时最容易遇到“偶发失败”尤其是网络慢导致元素加载超时。我推荐用pytest-rerunfailures插件给用例增加重试机制。但问题来了重试之后Allure报告里会显示多次执行记录状态到底是哪一次的呢Allure对重试有默认的合并逻辑如果最终重试成功了状态显示pass如果最终仍失败保留最后一次的失败信息。但在本地用allure serve时报告里仍然能看到之前的失败痕迹。此时如果希望重试成功的用例不再显示红色可以在allure-results目录下删除包含“retry”标识的json文件或者让Allure只保留最后一次的结果。比较省事的方法是每次重试都合并执行结果最后统一执行allure generate --clean报告里状态是最终状态。不过我要提醒一下重试次数不建议太多一般2次足够了。UI自动化的偶发失败大多是因为等待不足与其盲目重试不如把显式等待条件写对。重试只是兜底不是遮羞布。写在最后的小经验这一套TPshop UI自动化做完我自己最大的体会是报告不是给测试自己看的而是给整个团队看的。以前我跑回归开发就是一句“我跑的时候是好的”。现在有了Allure报告截图、步骤、日志全都挂在一起开发看一遍报告就知道是环境问题还是代码问题。这个习惯坚持下来测试在团队里的说服力会明显不一样。最后再分享一个小技巧如果你把allure-report发布到公司内部的静态服务器或者挂到Jenkins的Archive Artifacts里团队成员能直接通过链接访问手机上也能看。每次发版前把这个链接往群里一贴比发一百行log都管用。你真正上手跑一遍这套东西之后会回来感谢今天的自己。