从功能用例到自动化框架:用千问完成登录模块测试设计与Selenium+pytest落地
2026/9/24 23:48:15 网站建设 项目流程

第一次用千问写测试用例,是在一个后台管理系统的回归测试排期里。项目要做登录模块和用户管理模块的回归,手工用例堆了快两百条,还要同步补自动化脚本,时间压得很紧。我抱着“让AI先出个初稿,我只管改”的心态,把需求丢给了千问,结果这一试,就从功能测试用例一路折腾到了Python+Selenium+pytest+POM+数据驱动的自动化测试框架。整条链路跑通之后回头看,这次尝试最值的地方不是省了多少时间,而是让“功能用例”和“自动化用例”这两套原本各写各的东西,第一次在同一个工作流里对齐了。

这篇记录适合谁看?如果你也在用或打算用AI辅助测试工作,如果你正准备把手里的Selenium脚本整理成规范一点的框架,或者你只是想看看pytest的数据驱动到底怎么落地,这篇都能给你一些参考。里面我不会只贴代码,还会说清楚每一步为什么这么做、以及哪些地方AI帮不上忙,只能靠人拍板。

1. 从“问AI要几条用例”到“干脆搭个框架”:这件事的起因

先把当时的背景交代清楚。这个后台管理系统用的是经典的账号+密码+图形验证码登录,业务上有个特殊规则:员工账号首次登录必须强制改密,连续输错5次会锁定30分钟,管理员可以手动解锁。这些规则在需求文档里都有,但历史原因,测试用例文档里一直没有完整覆盖。

我一开始给千问的诉求很简单:帮我列登录模块的功能测试用例。当时我的想法是,网上关于登录用例的文章一抓一大把,AI训练语料里肯定不少,让它先出个通用清单,我再对照业务规则删改,比从零开始写效率高。

千问第一版返回的用例有40多条,分成了正常流程、异常流程、边界情况、安全情况四大类。说实话,覆盖面比我预想的广:账号空格、密码大小写、验证码刷新、弱密码校验、SQL注入、XSS注入、连续失败锁定、登录跳转、记住密码这些都有。那一刻我的第一反应是“早知道早点用AI”。

但紧接着我就发现了问题。AI写的用例有两条明显短板:一是它把所有系统当成同一个系统,很多用例在我这个项目里根本不存在,比如“第三方账号登录”“短信验证码登录”,我们压根没这功能;二是它完全不知道业务规则,比如“首次登录强制改密”这种核心场景,40多条用例里一条都没有。所以第一版用例我只能当成素材库,真正的用例清单,是在这个素材库基础上重新剪裁出来的。

也正因为这次体验,我意识到一个更值得做的事:既然AI能快速产出功能用例,那它能不能基于同样的逻辑,帮我生成自动化脚本?功能用例和自动化用例分开维护一直是我们团队的痛点——功能用例有几百条,自动化脚本只覆盖了其中一小部分,两边对不上。如果能让AI从功能用例直接带出脚本框架,哪怕只是个半成品,也能省不少事。

于是就有了后面这套Python+Selenium+pytest+POM+数据驱动的自动化框架。严格说,技术栈不是我拍脑袋定的,是我把团队现状、项目特点、自动化覆盖目标一起丢给千问,让它给出推荐方案,我再结合自己的判断做取舍。这个过程里,AI给的不是标准答案,而是帮我把选项摆得更清楚。

1.1 为什么第一次就敢让AI参与核心工作流

这里插一句大家可能关心的问题:让AI参与测试用例设计,甚至参与框架代码生成,会不会有代码安全风险?我的做法是,往AI里发的都是脱敏过的需求和结构片段,不涉及真实密码、不会把生产环境信息贴上去。页面元素定位符、页面结构这些属于项目前端代码的通用特征,风险相对可控。如果你所在企业对数据外发有明确限制,建议先确认合规要求,再决定能不能走这条路。

另外一个现实问题是,团队里不少新人第一次搭自动化环境就被python安装、selenium插件安装卡住。我也让千问整理了一份环境搭建清单,从装Python解释器到配置PATH、再到用pip安装selenium和pytest,一步步列出来。这东西本身不复杂,但作为第一次搭建的参考,帮我省了不少答疑时间。

2. 功能测试用例:千问的产出与我的三轮人工修订

想得到高质量的AI输出,提问方式比AI本身更重要。我第一次提问比较随意,结果返回的用例太泛,什么“验证用户界面美观”“检查页面响应速度”这种没法直接落地的条目都出来了。后来我把提示词改成了带业务规则和格式约束的结构化描述,输出质量才明显提升。

2.1 我给千问的提示词

当时用的是这样一段描述:

我正在测试一个后台管理系统的登录模块,技术栈是B/S架构,登录方式为账号+密码+图形验证码。 业务规则如下: - 账号错误或密码错误时,提示"账号或密码错误" - 连续输错5次密码,账号锁定30分钟 - 首次登录要求强制修改密码 - 修改密码时新密码不能与历史3次密码相同 - 验证码4位数字,点击可刷新,5分钟后过期 请基于以上规则设计功能测试用例,分为正常流程、异常流程、边界情况、安全情况四类, 每条用例包含:用例编号、用例标题、前置条件、测试步骤、预期结果、优先级。

加入业务规则和格式要求之后,输出质量明显提升。这个提示词的价值在于它把系统行为约束住了,AI不会再天马行空给你设计出“微信扫码登录”这种用例。这里有个细节:我特意说了“B/S架构”和“图形验证码”,因为这两条直接决定了后面自动化方案里要不要处理验证码、用什么方式驱动浏览器,提前告诉AI,它给的用例会更贴近可执行层面。

2.2 AI返回的核心用例

AI生成的条目经过我整理之后,大致是这个形态:

用例编号用例标题前置条件测试步骤预期结果优先级
TC-LOGIN-001正确账号密码登录成功已存在有效账号输入正确账号密码,点击登录进入系统首页,跳转至默认页面P0
TC-LOGIN-003账号为空登录账号留空,输入密码,点击登录提示“请输入账号”,不发起登录请求P1
TC-LOGIN-007密码连续输错5次账号锁定账号未被锁定连输5次错误密码第5次提示账号已锁定,锁定30分钟P0
TC-LOGIN-012首次登录强制改密新创建员工账号首次登录成功后进入修改密码页修改完成前无法使用其他功能P0
TC-LOGIN-018验证码过期页面停留超过5分钟输入账号密码,提交过期验证码提示验证码已过期,点击验证码图片可刷新P1
TC-LOGIN-023密码框输入SQL注入字符串密码框输入' OR '1'='1提示账号或密码错误,无异常数据返回P1

当然AI不会直接输出这么整齐的表格,格式是我统一过的。但每条用例的业务来源确实来自AI生成内容,我只是做了去重、归类和字段规范。这个工作量比从零写小得多,大概只花了三分之一的时间。

2.3 三轮人工修订都改了什么

第一轮是删。删掉不适用于本项目的用例,比如第三方登录、验证码自动识别这类不存在功能;同时把“页面加载是否美观”“操作是否流畅”这种不可量化条目直接打回。第二轮是精确化前置条件和预期结果。AI写预期结果经常是“提示密码错误”,但没说错误提示文案具体是什么,我全部改成了和需求文档一致的文案。第三轮是标优先级并对齐自动化覆盖矩阵:P0用例必须进自动化,P1争取自动化,P2暂不覆盖。

这里有一个很重要的心得。AI生成的用例,价值恰恰在“它不知道业务规则”这件事上。因为它不知道,它会按通用逻辑把所有可能性列出来,这能帮我把那些多年惯性思维下漏掉的边界点找出来。比如我做了几年登录模块测试,从来不测“密码为全空格”这种极端情况,AI列出来了,我实际一测,系统确实对全空格密码做了特殊处理,而需求文档里根本没写,这就是一个真实的业务逻辑缺陷。所以AI写用例的正确用法不是让它代替你做判断,而是让它帮你查漏补缺。

3. 自动化框架选型:Selenium、pytest、POM、数据驱动各自的角色

功能用例审完,开始考虑自动化。在选择技术栈之前,我先把功能用例里适合自动化的筛了一遍:回归频率高、步骤固定、预期结果明确的用例才值得自动化。像登录、用户创建、角色权限分配这种,完全适合;而涉及UI复杂交互的、需要人工判断视觉效果的,自动化性价比就低。这一步做完,有30条用例进入自动化范围,登录模块占了一大半。

3.1 为什么还是选Selenium

团队里之前有过一套零散的Selenium脚本,用的是unittest,脚本和用例逻辑混在一起,页面元素定位散落在各个函数里,改一个按钮的id要全局搜索替换。所以这次团队一度想换Playwright。我把这个问题拿去问千问,它的回答比较理性:如果团队已有Selenium积累、浏览器环境兼容要求高、项目短期内没有跨浏览器并行强需求,继续用Selenium是稳妥选择;Playwright的优势主要在自动等待、多浏览器支持、拦截网络请求这些能力上,但对团队有学习成本。最终我们从成本和稳定出发,保留了Selenium,但把代码结构完全重写。

这里我想多说一句。技术选型最怕的不是选错,而是反复横跳。Selenium在当前这个项目的定位就是“稳定够用”,它确实不像Playwright那样自带一堆现代特性,但它的社区语料足够多,AI对它的理解也最深,这意味着遇到任何问题,你都能快速得到靠谱的参考实现。对测试团队来说,可维护性比炫技重要得多。

3.2 pytest胜在fixture和参数化

测试框架从unittest迁到pytest,是这次最大的结构变化。pytest的fixture机制帮我解决了最头疼的两个问题:一是浏览器driver的创建和销毁,用session级别的fixture做一次启动,整个测试类共享;二是每次用例执行前要不要重新登录,用autouse的fixture控制。参数化更是天然适合数据驱动,后面章节会具体说。

我给千问的选择题是unittest还是pytest,它的回答很干脆:pytest。理由包括fixture比setUp/tearDown灵活、conftest.py做共享配置、参数化比ddt简洁、第三方插件生态丰富。这些判断我都是认同的。真正常用的pytest插件其实就三四个:pytest-html或allure-pytest生成测试报告、pytest-xdist做分布式执行、pytest-rerunfailures做失败重跑、pytest-ordering控制极端场景下的执行顺序。

3.3 POM模式的价值一张图说不清,但代码能说明

POM(Page Object Model)不是新技术,它的核心思想一句话就能讲清楚:一个页面一个类,页面上的元素定位和操作方法都封装在类里,测试用例只负责编排业务动作和断言结果。这样做的好处是,页面改了只改对应页面类;用例里永远不会出现driver.find_element这种裸调用,可读性和可维护性都会上一个台阶。

从AI的视角看,它理解POM的速度很快,甚至能直接给出BasePage、LoginPage的代码骨架。因为它的训练语料里有大量这类项目,所以这部分生成质量挺高,我只需要根据实际页面结构调整。这一点也是我建议大家在用AI写代码时优先选它做“脚手架”的原因——通用设计模式AI最擅长,业务特化逻辑才是你需要亲力亲为的部分。

3.4 数据驱动到底解决了什么问题

数据驱动的本质,是把“输入数据和预期结果”从测试脚本里拿出去,放到Excel、JSON或YAML这类外部文件里。测试脚本变成一套固定的执行逻辑,根据不同的数据组合反复执行。当时做这个决定,核心原因是登录用例有大量“用户名+密码+预期结果”的组合,如果每种组合都写成一条用例函数,代码会有一两百行重复;用数据驱动,代码只需要一个参数化函数,数据维护全在Excel里。

这个设计带来的收益是实打实的:新增一条用例不需要动代码,业务测试人员也能维护数据文件。更重要的是,前面辛辛苦苦整理的功能测试用例表,终于可以直接作为自动化数据源了。功能用例和自动化用例在Excel里共用一条记录,这就是标题里“功能测试用例与自动化测试用例”两套东西对上的关键点。

4. POM落地:登录模块从功能用例到页面对象的翻译全过程

选型定完,开始写代码。这个阶段我用千问生成了一版骨架,然后按照项目的真实页面结构调整。整体没什么惊险,但这个“翻译”过程值得记录,它正好展示了功能用例是怎么一步步变成自动化脚本的。

4.1 项目结构设计

目录结构是这样的:

project/ ├── config/ │ └── settings.py # 全局配置:URL、超时时间、浏览器类型 ├── data/ │ ├── login_data.xlsx # 测试数据文件 │ └── login_data.json # 备用数据文件 ├── pages/ │ ├── base_page.py # 页面对象基类 │ └── login_page.py # 登录页面对象 ├── test_cases/ │ ├── conftest.py # fixture和钩子 │ └── test_login.py # 登录模块测试用例 ├── utils/ │ ├── excel_reader.py # Excel读取工具 │ └── log_utils.py # 日志工具 ├── reports/ # 测试报告输出目录 └── requirements.txt

这个结构不是我凭空想的,我把需求丢给千问,它给了一版几乎相同的结构,我只加了config目录。目录分层的逻辑很简单:数据归数据、页面归页面、用例归用例、工具归工具,各层之间单向依赖,不允许用例直接操作浏览器API。这样新人接手时,看一眼目录就明白什么东西该放哪。

4.2 BasePage封装:把Selenium的原始操作包成业务动作

BasePage是所有页面对象的父类,封装Selenium最常用的操作。我最初版本几乎照搬了千问给的内容,后来又加了失败截图和日志,变成下面这样:

# pages/base_page.py import time import logging from pathlib import Path from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC logger = logging.getLogger(__name__) class BasePage: def __init__(self, driver, timeout=10): self.driver = driver self.wait = WebDriverWait(driver, timeout) def find_element(self, locator): """统一查找元素,等待元素可见后再返回""" return self.wait.until(EC.visibility_of_element_located(locator)) def find_clickable_element(self, locator): """查找可点击元素,用于按钮、链接等""" return self.wait.until(EC.element_to_be_clickable(locator)) def input_text(self, locator, text): """输入文本,每次输入前先清空""" elem = self.find_element(locator) elem.clear() elem.send_keys(text) logger.info(f"向 {locator} 输入文本: {text}") def click(self, locator): """点击元素""" elem = self.find_clickable_element(locator) elem.click() logger.info(f"点击元素: {locator}") def get_text(self, locator): """获取元素文本""" return self.find_element(locator).text def take_screenshot(self, name="screenshot"): """失败时截图,文件保存在 reports/screenshots/ 下""" timestamp = time.strftime("%Y%m%d_%H%M%S") report_dir = Path(__file__).parent.parent / "reports" / "screenshots" report_dir.mkdir(parents=True, exist_ok=True) filepath = report_dir / f"{name}_{timestamp}.png" self.driver.save_screenshot(str(filepath)) logger.info(f"截图保存至: {filepath}")

这里有两个容易踩坑的细节。第一个,find_element用了显式等待,而不是Selenium默认的隐式等待。隐式等待是轮询整个driver的,显式等待只作用于特定元素,两者混用会出现等待时间叠加,导致用例无故变慢。第二个,input_text里必须先clear()send_keys,因为不少输入框有默认值或上次会话残留,不清理直接输入,结果会跟你预期完全不一样。

4.3 LoginPage:把功能用例中的“步骤”变成“方法”

LoginPage对应登录页,把功能用例里的“输入账号”“输入密码”“点击登录”“获取错误提示”这些步骤,全部变成方法。元素定位符集中定义在类顶部的元组里,这是POM最重要的写法约定:修改定位符只需要改一处,不需要去脚本里满世界找。

# pages/login_page.py from selenium.webdriver.common.by import By from pages.base_page import BasePage class LoginPage(BasePage): username_input = (By.ID, "username") password_input = (By.ID, "password") captcha_input = (By.ID, "captcha") login_button = (By.ID, "loginBtn") error_tip = (By.CLASS_NAME, "error-tip") captcha_image = (By.ID, "captchaImg") def input_username(self, username): self.input_text(self.username_input, username) def input_password(self, password): self.input_text(self.password_input, password) def input_captcha(self, captcha): self.input_text(self.captcha_input, captcha) def click_login(self): self.click(self.login_button) def get_error_tip(self): return self.get_text(self.error_tip) def login(self, username, password, captcha="0000"): self.input_username(username) self.input_password(password) self.input_captcha(captcha) self.click_login()

这段代码看起来简单,但它是功能用例到自动化脚本的关键桥梁。功能用例写的是“输入正确账号密码,点击登录”,对应到这里就是login_page.login("admin", "123456")。用例层读起来像自然语言,每个方法只做一件事。尤其是login()这个组合方法,在后面所有的登录用例里都会被复用,这也是POM减少重复代码的典型体现。

我特意在login()里给验证码设置了一个默认值“0000”。这是测试环境专门配的万能验证码,后面讲验证码坑的时候会展开。这里想强调的是,代码里出现这种环境相关的魔法值时,一定要用默认参数和注释说明原因,否则三个月后你自己都忘了“0000”是什么东西。

4.4 conftest.py:fixture是pytest的灵魂

conftest.py放的是整个测试目录共享的fixture。浏览器启动、页面对象初始化、失败处理,全都在这里集中管理。

# test_cases/conftest.py import pytest from selenium import webdriver from pages.login_page import LoginPage @pytest.fixture(scope="session") def driver(): """启动浏览器,整个测试会话只执行一次""" options = webdriver.ChromeOptions() options.add_argument("--start-maximized") # 无头模式在CI环境使用,本地调试注释掉 # options.add_argument("--headless=new") driver = webdriver.Chrome(options=options) driver.implicitly_wait(2) yield driver driver.quit() @pytest.fixture() def login_page(driver): """每个用例独立获取一个登录页面对象""" page = LoginPage(driver) driver.get("http://your-test-env.com/login") return page @pytest.hookimpl(tryfirst=True, hookwrapper=True) 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: driver.save_screenshot(f"reports/screenshots/{item.name}.png")

fixture的作用范围(scope)需要仔细设计。driversession级别,是为了避免每个用例都重新启动浏览器,否则30条用例至少多花10分钟。但这也带来一个代价:用例之间共享同一个浏览器会话,一个用例改了登录状态,会影响后面用例。所以login_page这个fixture故意不设scope="session",让每条用例都重新访问登录页,确保起始状态干净。

4.5 用例层:像读功能用例一样读自动化脚本

有了页面对象和fixture,用例层的代码就非常干净了:

# test_cases/test_login.py import pytest class TestLogin: def test_login_success(self, login_page): """TC-LOGIN-001 正确账号密码登录成功""" login_page.login("admin", "123456") assert login_page.driver.current_url.endswith("/dashboard") def test_login_wrong_password(self, login_page): """TC-LOGIN-005 密码错误时提示账号或密码错误""" login_page.login("admin", "wrongpass") assert login_page.get_error_tip() == "账号或密码错误" def test_login_account_locked(self, login_page): """TC-LOGIN-007 密码连续输错5次账号锁定""" for _ in range(5): login_page.login("admin", "wrongpass") assert login_page.get_error_tip() == "账号已锁定,请30分钟后再试"

这里要注意,test_login_success里需要重新登录,所以即使前一条用例已经登录成功,这条用例也通过login_page这个fixture重新回到了登录页。这正是fixture设计时埋的伏笔:每个用例都从登录页开始,互不干扰。

看到这里你会发现,功能用例里的“测试步骤”和“预期结果”,在自动化里变成了“调用页面对象的方法”和“断言”。这就是标题里“功能测试用例与自动化测试用例”的翻译过程。AI在中间的贡献主要是把通用结构搭好,但每一条业务断言的具体值(比如错误提示文案)必须人工核对需求文档。

5. 数据驱动改造:测试数据搬进Excel之后发生了什么

前面代码里的用例数据都是写死的,login_page.login("admin", "123456")这种形式对固定测试场景没问题,但要覆盖前面整理的那30条功能用例,就得写30个类似的测试函数,显然不现实。数据驱动就是来解决这个问题的。

5.1 数据文件怎么设计

我用Excel作为数据载体,结构如下:

用例编号用例标题用户名密码验证码预期结果是否执行优先级
TC-LOGIN-001正确账号密码登录成功admin1234560000dashboardYP0
TC-LOGIN-003账号为空(空)1234560000请输入账号YP1
TC-LOGIN-005密码错误adminwrongpass0000账号或密码错误YP0
TC-LOGIN-007连续输错5次锁定adminwrongpass0000账号已锁定YP0
TC-LOGIN-012首次登录强制改密newuserinit1230000修改密码页YP0
TC-LOGIN-018验证码过期admin123456expired验证码已过期NP1

为什么用Excel而不是JSON?两个原因。第一,业务测试人员熟悉Excel,让他们维护测试数据基本零学习成本;第二,Excel可以很直观地做筛选、排序、标记是否执行,这在JSON和YAML里都没这么顺手。当然Excel在代码里读取时需要额外装openpyxl库,这是个小成本,换来的是业务人员能直接参与用例维护。

5.2 读取Excel的工具方法

# utils/excel_reader.py import openpyxl from pathlib import Path def read_excel_rows(filepath, sheet_name=None): """读取Excel每一行,返回字典列表""" filepath = Path(filepath) wb = openpyxl.load_workbook(filepath, data_only=True) ws = wb[sheet_name] if sheet_name else wb.active rows = list(ws.iter_rows(values_only=True)) if not rows: return [] headers = [str(h).strip() if h else "" for h in rows[0]] result = [] for row in rows[1:]: item = dict(zip(headers, row)) # 跳过标记为 N 的用例 if str(item.get("是否执行", "Y")).strip().upper() == "N": continue result.append(item) return result

这段代码有几个细节值得提。第一个,data_only=True是为了读取Excel里公式计算后的值,而不是公式本身,不然你读到的可能是=CONCATENATE(...)而不是结果。第二个,表头做了一次strip(),Excel里单元格经常带空格,不清理会让字典的key和后续代码对不上。第三个,“是否执行”字段的过滤逻辑放在读取工具里而不是用例层,这样所有数据统一遵守这个规则,不会有的用例读进来又不执行。

5.3 pytest参数化绑定

数据文件有了,下一步就是把它和pytest的参数化机制绑定:

# test_cases/test_login.py import pytest from utils.excel_reader import read_excel_rows LOGIN_DATA = read_excel_rows("data/login_data.xlsx") class TestLogin: @pytest.mark.parametrize( "case", LOGIN_DATA, ids=[f"{case['用例编号']}-{case['用例标题']}" for case in LOGIN_DATA] ) def test_login_with_data(self, case, login_page): username = case["用户名"] password = case["密码"] expected = case["预期结果"] login_page.login(username if username != "(空)" else "", password) current_url = login_page.driver.current_url if expected == "dashboard": assert current_url.endswith("/dashboard") elif expected == "修改密码页": assert current_url.endswith("/change-password") else: assert login_page.get_error_tip() == expected

这里的ids参数是容易被忽略但很重要的一点。没有它,pytest会把每条参数化用例命名为test_login_with_data[case0]test_login_with_data[case1],报告里完全看不出哪条用例是干什么的。加上ids之后,报告里显示的是test_login_with_data[TC-LOGIN-001-正确账号密码登录成功],一目了然。

这个测试函数里有一个明显的“业务翻译”逻辑:把数据文件里的“预期结果”转换成断言动作。比如dashboard代表断言当前URL以/dashboard结尾,修改密码页代表断言URL以/change-password结尾,其他文本则断言页面上的错误提示。这种转换逻辑放在用例层是合理的,因为数据文件要保持人能直接看懂的描述,而不是塞一堆current_url.endswith这样的技术细节。

5.4 数据驱动之后的用例管理

数据驱动落地之后,最直接的变化是:新增一条用例不需要写代码,只需要在Excel里加一行,填好编号、标题、数据、预期结果和执行标记。跑测试的时候,框架会自动读取所有标记为“Y”的用例并执行,报告里自动带上用例标题。这也是让业务测试人员参与自动化维护的前提——他们不需要理解pytest,只需要管Excel。

这里我也想提醒一句,数据驱动不是万能的。它适合参数组合较多、断言逻辑相对统一的场景,登录、查询、表单提交都适合。但如果用例的步骤差异很大,比如一条用例是登录,另一条是上传文件还带复杂的交互断言,强行塞进同一个参数化函数里,反而会让代码里塞满if-else分支。这时候老老实实拆成多个测试函数更清晰。我划分的标准是:断言逻辑是否一致,而不是数据是否相似。

6. 首次全流程执行:超时、验证码、数据污染,三个坑挨个排

框架写完,真正全流程跑的时候,问题一个接一个。这是整个过程中最有价值的一段,因为这些问题你在任何教程里都看不到完整的解决方案,但几乎每个跑Selenium+数据驱动的人都会遇到。

6.1 浏览器driver版本不匹配

第一跑就挂在环境上。本地浏览器是Chrome 120,但代码里selenium自动拉的driver版本不是对应的,直接报SessionNotCreatedException。如果你是用selenium 4以上版本,它带了Selenium Manager,理论上能自动匹配driver,但公司内网环境往往访问不了driver下载地址,这时候只能手动下载对应版本的chromedriver,放到PATH目录下。

处理方法不难:先看本地Chrome版本(chrome://version里能看),再去对应driver镜像站下载同大版本的driver,放到/usr/local/bin或Windows的Scripts目录,重新跑。这一步顺便排查了团队里新人的环境问题——很多人卡在“python安装好了但selenium跑不起来”,十有八九都是driver版本和浏览器版本对不上。

6.2 元素定位超时:显式等待的细节

环境通了之后,下一个遇到的是点击登录按钮时报ElementClickInterceptedException。原因不是定位符写错,而是页面上有个弹层遮住了登录按钮,Selenium虽然能定位到元素,但元素被遮挡,无法点击。这个场景在登录页很典型:一些系统登录前会先弹一个公告或者通知层,必须点掉才能操作。

我在BasePage里已经封装了find_clickable_element,它用element_to_be_clickable等待,这个条件不只是检查元素存在,还会检查元素可见且可点击。但它没法自动帮你关弹层。最后的解法是在登录页对象里加一个关闭公告弹层的方法,在login()里先尝试关闭:

def close_announcement_if_present(self): """关闭登录页可能出现的公告弹层""" try: close_btn = (By.CLASS_NAME, "announce-close") self.wait.until(EC.visibility_of_element_located(close_btn)).click() logger.info("公告弹层已关闭") except TimeoutException: pass # 没有弹层,忽略

try-except包住TimeoutException是关键,因为公告弹层不是每次都出现。这种“可能出现的元素”在自动化里很常见,处理原则是:能容忍的异常就捕获并处理,不要让它中断主流程。

6.3 验证码是个躲不开的坎

登录页有图形验证码,这在自动化里是个经典问题。AI给出的方案有好几个:OCR识别、对接打码平台、通过请求直接获取验证码值。但对内网测试系统来说,这些方案都太重了。最合理的做法是让开发在测试环境提供一个固定的万能验证码,比如“0000”,所有自动化用例统一填这个值。

如果测试环境实在改不了验证码逻辑,还有一个相对稳的做法:登录接口单独开一个白名单,自动化的请求跳过验证码校验。但这需要开发配合,而且白名单本身有安全风险,生产环境绝对不能开。总之,验证码问题的核心不是技术,而是沟通——尽早和开发约定测试环境的验证码策略,能省下大量时间。

6.4 数据污染:用例之间互相影响的教训

跑数据驱动用例时发现,TC-LOGIN-012“首次登录强制改密”执行之后,后面的登录用例开始失败。原因很直接:这条用例用账号newuser首次登录后,系统强制跳到了改密页,newuser的密码已经被修改,后续用例如果再用newuser+旧密码登录,当然会失败。这就是典型的数据污染。

解决办法有两个方向。一是在用例前置里做数据清理,确保每个账号的初始状态已知;二是给每个用例使用独立的测试数据,比如用随机后缀创建新账号。对于这个登录场景,我最终选择了前置数据处理:在执行强制改密用例前,通过数据库或接口把newuser的密码重置回初始状态。另外,执行顺序也有讲究,我在pytest里用@pytest.mark.run(order=1)把这条用例固定在前面,避免它影响其他数据。

6.5 失败重跑和报告配置

最后是稳定性的问题。30条用例第一次跑下来,有3条失败,但单独重跑这3条又全过。这属于典型的偶发失败,多半是网络延迟或元素加载慢。我给关键用例加了@pytest.mark.flaky(reruns=2),重跑两次如果再失败才判失败。pytest-rerunfailures这个插件很轻量,用法就是这么一行。

报告我用的是pytest-html,配置很简单,在pytest.ini里加几行:

[pytest] addopts = -v --html=reports/report.html --self-contained-html testpaths = test_cases

--self-contained-html这个参数很重要,它会把CSS和JS都打进HTML文件里,报告可以直接发给别人,不用附一堆静态资源。配合前面conftest.py里的失败截图钩子,报告里能看到每一条失败用例当时页面的样子,排错效率高了一大截。

7. 用AI写测试用例这件事,我的真实体会

整个流程走完,我对“AI辅助测试”这件事的认知变了很多。它不是简单的“让AI替你写用例”,更准确的说法是“让AI做初稿,人做判断”。AI最强的能力是用它海量的语料积累,把我可能忽略的边界情况、通用场景、代码结构一次性摆到桌面上来;但AI不具备任何关于你这个具体系统的业务知识,也不知道你们团队历史上踩过哪些坑。这两个特点决定了它适合做助手,不适合做决策者。

具体说,AI在这些环节真正的帮助最大:生成功能用例初稿、搭POM框架的脚手架、解释Selenium某个报错的含义、把日志里的异常翻译成人话。而在判断业务规则对不对、决定某种数据组合是否合理、区分哪些用例必须P0优先级这些地方,AI给不了任何有效建议,只能靠测试人员基于对项目的理解来拍板。

提示词的使用也有几个心得。第一,给上下文比不给上下文好得多,把业务规则、技术栈、格式要求一次性写清楚,AI的输出质量会高一个档次。第二,逐步追问比一次性问完更有效,我会先让它列功能用例,再让它从中挑适合自动化的,再让它按POM结构生成代码,每一步都在上一步基础上深化。第三,让它输出结构化内容(表格、代码、列表)比让它写长文更实用,方便我直接复制到文档和工程里。

回到我一开始的预期:想省时间。这件事AI确实做到了。更让我觉得值得的是,功能用例文档、自动化数据、测试报告这三样东西,现在完全对齐了。以前功能测一遍、自动化脚本再写一遍,两边数据和预期结果经常对不上,现在维护同一个Excel,跑一次自动化,报告直接说明每个功能用例在自动化里的表现。对我来说,这才是AI参与测试工作流最大的价值。

最后再说一个使用上的小建议:AI给的用例和代码,一定要亲自跑一遍、逐条看一遍再合入。我第一次跑框架时,AI生成的代码里就有几处明显的低级错误,比如变量名拼写不一致、循环里漏了递增条件。它写代码的能力再强,也替代不了人工审查这一关。把AI当成一个工作效率放大器,而不是一个可以完全托付的同事,用起来才会顺手。

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

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

立即咨询