☰
TPshop实战:Pytest+Selenium+Allure打造漂亮UI自动化测试报告
2026/9/25 4:16:11 网站建设 项目流程

很多人学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?m=Home&c=User&a=login就是登录页,c=Cart就是购物车,一眼就能看出用例对应哪个模块。再加上这些页面用了大量稳定的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.13,Windows和macOS跑都没问题。Python 3.10在类型注解和语法上比3.7更舒服,Selenium 4自带相对定位器和改进的等待API,写起来比3.x简洁。Pytest 7对fixture和钩子的支持很好,反正稳定版优先,不要追求每个库都是最新。

Allure命令行工具是生成报告的关键。在macOS上可以用brew install allure,Windows可以用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大致长这样:

pytest==7.4.0 selenium==4.15.0 allure-pytest==2.13.0 webdriver-manager==4.0.0

webdriver-manager这个库强烈建议装上,它能自动下载和匹配浏览器驱动版本,省去手工折腾chromedriver的麻烦。装完依赖后,在项目根目录新建一个pytest.ini,内容如下:

[pytest] addopts = -s -q --alluredir=allure-results testpaths = testcases

这里的关键是--alluredir=allure-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级的浏览器fixture:

import pytest from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.service import Service @pytest.fixture(scope="class") def browser(): options = webdriver.ChromeOptions() options.add_argument("--window-size=1920,1080") options.add_argument("--disable-gpu") driver = webdriver.Chrome( service=Service(ChromeDriverManager().install()), options=options ) driver.implicitly_wait(5) yield driver driver.quit()

这里用scope="class",让同一个测试类里的用例共用浏览器,速度快很多;如果多个用例必须互相独立,再改成scope="function"。隐式等待设成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_wait=10): 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[@class='btn-search']") first_goods_loc = (By.XPATH, "//div[@class='shop-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?m=Home&c=User&a=login") 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/ --alluredir=allure-results

然后另开终端,生成并打开报告:

allure generate allure-results -o allure-report --clean allure open allure-report

第二种是直接一把梭,跑完自动打开浏览器看报告:

pytest testcases/ --alluredir=allure-results allure serve allure-results

allure 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(hookwrapper=True) 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, name=name, attachment_type=allure.attachment_type.PNG)

也可以顺带把页面HTML抓下来,便于在报告里直接审查DOM结构:

html = driver.page_source allure.attach(html, name="page_source", attachment_type=allure.attachment_type.HTML)

日志也同理,用logging模块记录关键步骤,在失败时把日志内容attch为TEXT。这样一份报告里既能看到步骤,又能看到截图,还能看日志,开发不用再去测试那边翻聊天记录。

4.4 历史趋势与清理策略

Allure报告最有意思的一点是能展示测试执行的历史趋势,包括用例总数、失败率、耗时趋势。但这个功能有个前提——必须保留上一份报告生成的history目录里的数据。很多人发现自己的报告里“History”页签为空,就是因为上一份报告已经被--clean删掉,或者根本没生成历史数据。

正确的做法是:第一次生成报告后,allure generate --clean会得到一个新的allure-report目录,目录下会有history文件夹。下一次跑用例前,把上一次报告目录里的history文件夹整体拷贝到新的allure-results目录下,然后再执行pytest --alluredir=allure-results,最后重新generate,历史趋势就延续下来了。

在本地调试时,我没那么讲究,通常直接删除allure-results目录重跑,不关心历史。但如果是在Jenkins里保存构建产物,就需要用一条命令来完成历史清理和拷贝。我给个参考脚本:

rm -rf allure-results cp -r allure-report/history allure-results/history pytest testcases/ --alluredir=allure-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 --alluredir=allure-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 PYTHONUTF8=1,强制Python以UTF-8模式运行。这一条能解决80%的中文乱码问题。

5.3 失败没截图,排查全靠猜

就算我们在conftest里写了失败自动截图,还是有同学跟我说报告里看不到图。仔细一看,截图函数里的driver参数根本不是同一个实例。比如用了多个fixture,有的case拿的是browser,有的case拿的是webdriver,hook里只取了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都管用。你真正上手跑一遍这套东西之后,会回来感谢今天的自己。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询