Web自动化测试从零到一:Selenium+pytest+Allure框架落地实践

发布时间:2026/10/21 10:21:06
Web自动化测试从零到一:Selenium+pytest+Allure框架落地实践 Web自动化测试聊起来往往比做起来热闹。真正把用例写好、执行稳定报告能直接拿去汇报这一整条链路跑通之后你才会意识到自动化并不是把Selenium装起来就完事。今天这篇就以我最近在维护的一个Web后台项目为例把从用例设计、元素定位、数据驱动到Allure报告和CI定时任务从零到一的过程完整拆一遍。这篇文章适合谁刚上手Web自动化、写了不少脚本但总觉得很脆的人已经在用脚本但维护成本极高、页面改一个按钮要翻大半天代码的人或者手上正好要选型想看看Selenium加pytest加Allure这套主流组合到底怎么落地的人。我会尽量把每个决定背后的原因讲清楚也会把自己踩过的坑直接摊开。1. 项目概述与整体破题思路1.1 动手之前先想清楚的三件事Web自动化测试最常见的翻车现场是一上来就录脚本然后过两个版本全红。所以我做任何自动化项目第一步都不是写代码而是先把三件事定下来范围、层级、可维护性标准。范围说的是不能一口气把所有页面都自动化。实际项目里适合自动化的往往是回归频率高、流程稳定、逻辑相对固定的场景比如登录、权限变更、核心订单流程、报表导出这类。而图片裁剪、上传素材这种交互频繁改动的功能可以往后放一放等版本稳定了再补。范围选得太大框架还没成型就被各类异常拖垮范围选得太小又体现不出自动化的价值。我一般会先从冒烟用例开始保证一到两条关键业务链路每天夜里能跑通。层级说的是用例结构。脚本也好框架也好最好分成三层用例层只写业务意图页面对象层封装元素和操作执行层统一处理等待、截图、日志、重试。分层的主要目的是页面改版时只改一处而不是满世界替换定位符。没有分层逻辑最典型的现象是一个登录用例里散落着十几个By.ID不同脚本里各写一套等待逻辑互相抄来抄去最后变成谁也不敢动的大泥潭。可维护性标准很好理解一个用例从发现问题到修完重新执行如果花费的时间超过手工回归一次这个自动化就失去了意义。所以从第一行代码开始就要考虑元素定位的稳定性和用例之间的独立性。标准定得越具体后面维护的时候越不会被杂事牵着走。1.2 技术选型为什么是Selenium加pytest加Allure做Web自动化的可选工具很多Cypress、Playwright、Robot Framework都有各自的拥趸。我最后选了Selenium加pytest加Allure不是因为他们最强而是他们满足了一个很朴素的条件成熟、可控、团队容易接手。Selenium的WebDriver生态最老碰到疑难案例时社区资料最齐全团队里有经验的人也多。Playwright确实快自带录屏和Trace Viewer但如果项目里有大量历史脚本要迁移改造成本反而成了主要风险。pytest则负责用例组织fixture做浏览器和测试数据的自动管理参数化做数据驱动插件机制挂截图、失败重试、并发执行基本不用做二次封装。Allure负责报告它的趋势图和结果聚合能直观看到每个版本的通过率变化也能把用例步骤、日志、截图整理成一份能直接发给产品团队的文档。组合上手难度生态丰富度报告能力适合场景Selenium pytest Allure中等高强跨浏览器兼容、已有脚本迁移Playwright中等增长快自带Trace新项目、需要精细控制网络请求Robot Framework低较高自带报告偏业务团队、关键字驱动选型没有标准答案最重要是团队能把控。我不太建议在自动化框架初建期引入几种工具来回切换先把一条链路跑稳再考虑是不是要换更时髦的方案。2. 工程骨架搭建目录、依赖与浏览器管理2.1 目录结构与依赖准备一个看起来不复杂、但能支撑长期迭代的项目结构我习惯这样放web_auto_test/ ├── pages/ # 页面对象 │ ├── base_page.py │ └── login_page.py ├── test_cases/ # 用例 │ └── test_login.py ├── data/ # 测试数据 │ └── login_data.yaml ├── utils/ # 公共工具 │ ├── driver.py │ ├── logger.py │ └── screenshot.py ├── reports/ # 报告与附件 ├── conftest.py ├── pytest.ini └── requirements.txtpages目录放页面对象用例目录只放测试函数data目录集中管理账号、URL、业务数据utils放和业务无关的基础能力比如驱动创建、日志、截图。将来人数变多了按模块继续拆test_cases不会互相干扰。依赖方面我的requirements.txt通常会包含这几样selenium4.x.x webdriver-manager4.x.x pytest8.x.x allure-pytest2.13.x pytest-xdist3.x.x pytest-rerunfailures14.x.x pyyaml6.xselenium是主驱动webdriver-manager负责自动下载匹配浏览器版本的driver省去了手工维护driver的麻烦。pytest加allure-pytest负责执行和报告输出pytest-xdist用于并发pytest-rerunfailures用于偶发失败重试。这里容易踩的坑是版本对齐尤其selenium大版本升级之后很多等待条件和参数行为会有变化代码里尽量少依赖那些特别直白但未来会废弃的写法。2.2 浏览器生命周期管理conftest一把梭pytest的fixture机制很适合管理浏览器实例。不管用例成功还是失败最后都要把浏览器关干净这种需求用yield fixture再合适不过。我习惯在conftest.py里这样写import pytest from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager pytest.fixture(scopefunction) def driver(): service Service(ChromeDriverManager().install()) options webdriver.ChromeOptions() options.add_argument(--start-maximized) options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) # 在无显示器的CI机器上可以打开无头模式 # options.add_argument(--headlessnew) driver webdriver.Chrome(serviceservice, optionsoptions) yield driver driver.quit()scope设为function是让每个用例拿到独立浏览器避免相互污染。有些团队为了省时间把浏览器做成session级复用一旦用例变多页面状态残留、登录态混乱的问题就会特别痛苦。我的取舍很明确宁可一次执行慢一点也要保证每个用例的起点是干净的。并发提速可以通过pytest-xdist来做而不是靠复用同一个浏览器。如果你用Firefox或者Edge只需把对应的option和driver类换掉骨架完全一样。这里还有个细节CI机器的环境变量里没有浏览器时可以把driver下载目录设置到固定路径避免每次执行都重新下驱动减少流水线时间。3. 用例设计与元素定位的核心细节3.1 Page Object建模别让定位符散落一地模板化的页面对象到底怎么落地是很多新手容易混乱的地方。我把BasePage作为所有页面的父类把公用的显式等待、点击、输入、截图都封装在里头from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException class BasePage: def __init__(self, driver, timeout10): self.driver driver self.default_timeout timeout self.wait WebDriverWait(driver, timeout) def find_element(self, locator, timeoutNone): timeout timeout or self.default_timeout try: return WebDriverWait(self.driver, timeout).until( EC.visibility_of_element_located(locator) ) except TimeoutException: self.take_screenshot() raise注意find_element里等待可见性click里则等待可点击这两个条件并不等价。有些元素出现在DOM里但被遮挡或者还在加载动画中如果只用visibility_of_element_located点击时就会莫名其妙地失败。所以在点击场景我单独写一个方法def click(self, locator): element self.wait.until(EC.element_to_be_clickable(locator)) element.click()页面类里只存放定位符和业务操作不放断言。以登录页为例from selenium.webdriver.common.by import By from pages.base_page import BasePage class LoginPage(BasePage): username_input (By.CSS_SELECTOR, input[nameusername]) password_input (By.CSS_SELECTOR, input[namepassword]) login_button (By.CSS_SELECTOR, button[typesubmit]) error_message (By.CSS_SELECTOR, .el-message--error) def login(self, username, password): self.find_element(self.username_input).clear() self.find_element(self.username_input).send_keys(username) self.find_element(self.password_input).clear() self.find_element(self.password_input).send_keys(password) self.click(self.login_button)这样一来用例层永远只看到login_page.login这种业务方法页面结构全被隔离在Page Object里。将来前端给用户名输入框改名了只要在LoginPage里改一行不用去几十个用例里替换。3.2 等待策略稳比快重要显式等待优先影响自动化稳定性的头号因素是元素没有及时加载出来。默认的find_element是立即寻找找不到就抛NoSuchElementException。正确的做法是显式等待在规定时间内反复轮询某个条件条件满足再往下执行。我几乎不在项目里用隐式等待原因很简单隐式等待是对所有find_element统一生效粒度太粗很容易出现“网页还在加载某个元素明明不可点击但find_element已经返回”这种尴尬局面。更麻烦的是如果同时设置了隐式等待和显式等待遇到极端情况时总等待时间会叠加排查起来更难。time.sleep更不推荐它会让执行时间凭空拉长而且掩盖实际时机。显式等待的条件选择也有讲究。元素存在、元素可见、元素可点击是三个层次要根据业务动作选合适的。表单提交按钮在提交后通常会短暂变成loading状态如果断言太快就会抓到一个半新半旧的结果这时候等一个包含“预期文案”的元素出现比单纯sleep靠谱得多。建议把常用的等待条件都封装到BasePage里测试用例里尽量不要出现EC.xxx这种细节。4. 用例编写、参数化与失败现场保留4.1 一个可读可维护的用例模板用例层应该长得像操作步骤说明书。拿登录模块举例import pytest from pages.login_page import LoginPage pytest.mark.parametrize(username,password,expect, [ (admin, 123456, 登录成功), (admin, wrongpass, 用户名或密码错误), ]) def test_login(driver, username, password, expect): page LoginPage(driver) page.open(https://your-test-environment.com/login) page.login(username, password) assert expect in page.get_message_text()这个用例具备三个特征一是每条用例独立前置条件通过open登录页重新打开二是断言贴近业务语义只看最终提示不去检查一堆中间DOM状态三是参数和用例业务分离后续加数据不用改代码。这里open方法是在BasePage里实现的它会先执行driver.get(url)再把driver清cookie还是保留登录态由业务场景决定控制权留给上层。用例粒度我一般控制在“一个场景一行断言”。如果一条用例里同时校验登录成功、跳转地址、菜单权限、页面标题一旦中间一步失败定位问题就得从头看。拆成多条断言更清晰但也要适度别为了拆分把流程反复重跑。4.2 数据驱动YAML管理测试数据数据驱动能让用例数据和控制逻辑彻底分开。登录数据放在data/login_data.yaml里格式类似valid: - username: admin password: 123456 expect: 登录成功 invalid: - username: admin password: wrongpass expect: 用户名或密码错误测试文件里加载YAML并生成参数from pathlib import Path import yaml def load_login_cases(): raw yaml.safe_load(Path(data/login_data.yaml).read_text(encodingutf-8)) cases [] for group in (raw[valid], raw[invalid]): for item in group: cases.append((item[username], item[password], item[expect])) return cases pytest.mark.parametrize(username,password,expect, load_login_cases()) def test_login(driver, username, password, expect): # 用例内容保持不变 pass这种设计的好处是产品调整了提示文案测试人员只要改YAML新增一组异常账号也只要在YAML里加一条。数据文件多了以后建议每个模块一个文件并且命名和用例模块一一对应免得后续找不到。失败重试最好和重试原因绑定。我的做法是设置最多重试一次并且把重试记录打印到日志里。为什么不敢多设如果用例本身断言不合理重试3次只会多拖3倍时间反而把真正需要关注的问题藏住。重试只应对偶发的网络抖动、页面加载超时这类环境问题不放三倍放两倍等统计出重试成功率高的时候再去排查根因而不是无限容忍。4.3 截图、日志与失败现场还原失败现场信息越多排查成本越低。我在BasePage里加了两个方法测试过程中随时可以调用import allure def take_screenshot(self, namescreenshot): png self.driver.get_screenshot_as_png() allure.attach(png, namename, attachment_typeallure.attachment_type.PNG) def attach_page_source(self, namepage_source): html self.driver.page_source allure.attach(html, namename, attachment_typeallure.attachment_type.HTML)再配合pytest的钩子在用例失败时自动保存pytest.hookimpl(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: allure.attach(driver.get_screenshot_as_png(), namefailure_screenshot, attachment_typeallure.attachment_type.PNG) allure.attach(driver.page_source, namefailure_page_source, attachment_typeallure.attachment_type.HTML)截图是给眼睛看的HTML源码是给后续分析用的。有些问题只发生在某个异步请求返回之后光看截图看不出来但打开page source搜索一下关键文案就能判断请求到底有没有返回。日志方面我习惯在每个用例的关键步骤里加logger.info带上当前用例的标识这样报告里能看到完整动作链而不是孤零零一个失败断言。5. 测试报告生成与CI集成5.1 Allure报告接入与历史趋势执行时用--alluredir指定中间结果目录然后generate生成网页报告pytest test_cases --alluredirreports/allure-result allure generate reports/allure-result -o reports/allure-report --clean allure open reports/allure-reportAllure的静态报告里能看到通过率、失败用例、用例步骤、每个步骤的耗时、参数值和附件。比普通HTML报告强一点的地方在于它知道每个用例是从哪条参数来的同一方法的不同参数会展开成多条记录非常直观。想让报告带历史趋势需要保留上一次生成的history目录if [ -d reports/allure-report/history ]; then cp -r reports/allure-report/history reports/allure-result/ fi allure generate reports/allure-result -o reports/allure-report --clean这样连续跑几天之后报告首页会出现趋势图和通过率曲线。这个趋势对回归节奏的判断特别有用如果某个模块的失败率在缓慢爬升大概率是页面改版引入了脏数据或者自动化脚本没有及时跟上业务变更。等到整条链路全红了才去看往往已经晚了三到四个版本。5.2 在CI上跑自动化调度、并发与结果通知自动化不能只在本地跑早晚要挂到CI上定时执行。我在CI环境里一般分四个阶段安装依赖、执行用例、生成报告、归档结果。Jenkins里可以这样写核心流水线pipeline { agent any triggers { cron(H 2 * * *) } stages { stage(Install) { steps { sh pip install -r requirements.txt } } stage(Test) { steps { sh pytest test_cases --alluredirreports/allure-result } } stage(Report) { steps { sh if [ -d reports/allure-report/history ]; then cp -r reports/allure-report/history reports/allure-result/ fi allure generate reports/allure-result -o reports/allure-report --clean } } stage(Archive) { steps { archiveArtifacts reports/allure-report/** } } } }注意几点第一调度时间最好选在业务低峰期因为自动化跑通之后会产生大量测试数据避免影响正常业务第二归档产物只保留最近N次否则存储很快会被报告填满第三如果加了pytest-xdist并发要确保CI机器上能同时创建多个浏览器进程内存小的机器建议限制最大并发数。另外CI节点上要提前装好allure命令行工具或者通过构建步骤安装否则Report阶段会直接失败。失败通知我一般分两层CI平台自带的邮件或即时消息通知负责“用例失败了”这个粗粒度告警Allure报告里的失败详情负责具体定位。通知文案不要只发“有失败用例”最好带上报告链接让相关的人直接点进去看日志和截图。多跑几天后你可能会发现真正需要人工介入的失败比例会比想象中低很多很多是数据脏了或者环境没恢复这时要在脚本里增加数据自清理能力而不是一条条手工处理。6. 实战踩坑实录定位不到、元素失效与并发问题6.1 元素定位不稳定的实战解法页面元素定位是Web自动化里最日常也最头疼的事。我现在的原则是优先级从高到低稳定的业务属性比如data-testid大于id大于name大于不可重复的CSS路径最后才是XPath。XPath不是不能用但容易写出特别长的绝对路径比如/html/body/div[3]/div[1]/form/div[2]/input页面一调整就全废。如果非要用建议用相对路径配上关键属性比如//button[contains(text(),立即登录)]。CSS选择器比XPath短小但需要前端配合给关键控件加上可读性高的属性不然同样会翻车。推荐做法是推动测试环境给通用组件加data-testid。按钮、输入框、表单项这些高频操作对象加一次属性一劳永逸。例如confirm_button (By.CSS_SELECTOR, [data-testidconfirm-button])这个属性的最大好处是它和样式、文案、DOM层级都无关前端改视觉、改文案都不会破坏用例。当然这需要和前端团队协商好把它当成接口承诺而不是可以随手删掉的临时属性。6.2 StaleElementReferenceException与动态刷新这是老生常谈但每次遇到还是会让新人头疼。背后的原理很简单页面元素被重新渲染之前的引用已经失效。常见场景是点击一个按钮后列表重新加载原本定位到的行元素被替换掉了。我的处理方式有两种。第一种操作前重新定位元素不把find_element的结果存在变量里反复用。第二种封装一个带重试的业务点击方法from selenium.common.exceptions import StaleElementReferenceException from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def click_with_retry(self, locator, retries3): for attempt in range(retries): try: element WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable(locator) ) element.click() return except StaleElementReferenceException: continue raise TimeoutException(元素在多次重试后仍然失效)重试本身不是银弹如果3次之后还是失败说明页面状态判断逻辑有问题要去检查是不是点了某个按钮后出现了弹窗遮挡或者请求迟迟没返回。之前遇到一个很隐蔽的场景表格页每10秒自动刷新一次用例刚好在刷新瞬间切入元素刚好失效。后来把刷新定时器暴露成测试开关测试环境关掉它问题就消失了。这个思路可以推广与前端约定测试专用入口远比在脚本里反复等待更可靠。6.3 并发执行时如何保证用例独立性用pytest-xdist加-n 2之后执行时间能砍半但会暴露出一堆串行执行看不出的问题。最典型的是测试数据冲突几个用例同时用同一个测试账号登录服务端强制踢下线所有用例一起挂。解决思路是让每个预期数据都独立生成比如账号带随机后缀用例结束直接把数据清掉不要依赖公共数据池。还得注意浏览器环境的隔离。conftest里fixture是function级每个并发worker各自的浏览器进程这条路已经保证了基本隔离。真正容易忽略的是某些用例会操作全局配置比如修改系统参数、切换语言环境这类用例并发时就可能影响其他用例。最简单的做法是这类操作单独放一个目录用pytest的mark标记CI上先跑普通用例再跑带e2e标记的用例避免互相打断。用例排序依赖更要避免。自动化用例不要假设“登录用例永远先执行成功”每个用例如果需要登录要么自己调用登录接口拿token要么在本用例里重新走一遍登录。虽然会多一点执行时间但换来的是可以随意打散、随意分配并发这个收益很值。7. 日常维护里比写脚本更重要的事前面这些章节已经把一条从用例到报告的链路铺开了。最后想聊点技术之外、但实际影响比框架选型更大的事。我经历过自动化跑了一周后通过率从95掉到70的情况后来一查不是用例代码坏而是测试环境里一批老数据到期登录后的默认页面多了一个弹窗。这类问题靠修定位符治标不治本根本办法是把环境状态、测试数据、前端自测开关当成自动化的一部分来治理。所以现在每接手一个项目我都会顺手维护一份“环境与数据准备清单”哪些账号可以用哪些数据必须在执行前生成哪些历史数据会干扰用例结果。脚本每次启动时先对关键前置条件做一次自检缺少就自动创建创建不了就明确报错。这个步骤虽然只有十几行代码但能让无效失败率明显下降。关于要不要上UI自动化我的态度一直是这样不要被“自动化程度越高越好”这句话绑架。UI自动化适合做回归验证、端到端流程保障但不适合替代所有手工测试。一个按钮的颜色和文案是否正确用代码检查远不如人眼扫一眼快硬要自动化反而增加维护成本。真正好用的自动化是让人觉得“每天早起看一条报告链接顺便就把回归做了”的东西是一台安静可靠的机器而不是一场声势浩大的革命。做到这个程度靠的不是某个惊艳的库而是对项目业务的理解和持续把脚本收拾干净的日常功夫。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询