☰
Pytest+Selenium实现Web登录自动化测试:从用例设计到稳定运行的完整实践
2026/10/18 8:09:41 网站建设 项目流程

最近接了个活儿,用 Pytest + Selenium 给一款 AI 助手 Web 端的登录功能做自动化测试,也就是大家常说的 DeepSeek 登录模块那一类场景。这篇文章不打算写“hello world”式的入门教程,而是把这套登录自动化里从需求拆解、用例设计、代码实现到问题排查的完整过程摊开来说。适合谁看?如果你是刚开始做 Web 自动化、或者手动测试做腻了想往自动化方向靠,又或者已经会写一点脚本但总被登录这种动态页面坑得怀疑人生,那下面的内容应该对你有用。登录功能看起来简单,做起来要多想几层,不写自动化你根本意识不到它有那么多边角料问题。

1. 项目背景与测试目标

1.1 登录模块为什么值得单独做自动化

很多团队做自动化测试,喜欢先从核心业务链路下手,比如搜索、下单、支付这些。但我的经验恰恰相反:登录应该是第一个被自动化的模块。原因很简单,登录是所有用户行为的前置条件,一旦它出问题,后面什么业务都跑不起来。而且登录模块的变化频率并不低,前端改个样式、后端调个验证逻辑、加个第三方登录入口,都可能把原有流程打乱。

更重要的是,登录功能表面上只有一个输入框加一个按钮,实际上它的分支逻辑非常多:账号不存在、密码错误、账号被锁定、验证码过期、手机号格式不对、第三方授权失败、会话超时。这些分支用手工测试去回归,一轮下来至少需要二十分钟,而且容易漏。自动化脚本可以把这些边界场景在几分钟内全部跑完,还能在每次改版之后立刻回归一遍。

我在做这套用例之前,先统计了一下手工回归登录模块的耗时。常规操作是打开网页、输入测试账号、等验证码、点登录、再退出,五到六个来回,大概需要十五到二十分钟。如果要把异常场景也覆盖上,耗时会翻倍。自动化之后,同样的覆盖范围压缩到三到五分钟,关键是它可以半夜跑,早上上班直接看结果。

1.2 为什么选 Pytest + Selenium 而不是别的组合

选型这个问题,我几乎没有纠结。Selenium 是目前 Web UI 自动化最成熟的方案,它支持主流浏览器,生态完善,遇到问题随便一搜就能找到解决方案,这对测试脚本来说非常重要。相比 Playwright 和 Cypress,Selenium 的语法虽然显得啰嗦一些,但它的稳定性和团队上手成本是实打实的优势。很多测试团队里并不是人人都会写代码,Selenium 的老牌地位意味着能找到大量现成的参考实现,学习曲线更平缓。

Pytest 这边就更好选了。相比 Python 自带的 unittest,Pytest 的断言更简洁,fixture 机制处理测试前置和后置条件非常顺手,参数化功能天然适配登录这种多组数据验证的场景。而且 Pytest 的插件生态非常丰富,pytest-html、allure-pytest、pytest-xdist 这些都能直接满足报告、并发、重试的需求。

有人可能会问,为什么不直接用 Robot Framework?Robot Framework 的关键字驱动风格适合业务同事一起参与,但到了复杂逻辑处理的时候,还是要写 Python。既然最终都要写代码,那不如直接用 Pytest,灵活度更高。也有人问为什么不用 Requests 直接调接口测登录,那样确实更快更稳,但接口测试替代不了 UI 层面的验证,比如按钮是否可点击、错误提示是否在正确位置展示、登录成功后页面元素是否正常渲染。最合理的做法是 UI 和接口测试分层,但在项目早期,先把 UI 自动化跑起来,能最快暴露前端的问题。

1.3 测试范围划定与用例设计策略

登录模块测试范围的划定,我建议遵循两个原则:第一,UI 自动化只覆盖关键路径和风险高的分支,不要试图覆盖所有异常输入组合;第二,数据和用例分离,把输入输出放在参数化数据里,后续加用例不用改动脚本逻辑。

我在这个项目里把登录用例分为四大类:

  • 正常登录场景:正确的用户名和密码,验证码正确,登录成功后跳转到首页,并展示用户昵称。
  • 异常输入场景:空用户名、空密码、错误密码、用户名不存在、密码长度超过边界、包含特殊字符等。
  • 环境限制场景:网络超时、登录接口返回异常、页面刷新后 token 失效等。
  • 交互体验场景:登录按钮在未输入内容时是否置灰、输入错误后是否有明确提示、点击登录是否重复提交。

每种场景下再细分具体用例。这里有一个很容易犯的错误,就是用例数量贪多。UI 自动化的用例并不是越多越好,因为每一个用例都依赖浏览器执行,跑起来慢,而且元素变化容易挂。我一般控制登录模块的 UI 用例在二十条以内,跑完控制在五分钟左右,超过这个规模就说明用例粒度太细,需要考虑拆到接口层去覆盖。

2. 环境准备与工程架构

2.1 本地环境搭建与版本匹配

这套测试项目用到的环境依赖非常标准,Python 3.10 以上,Selenium 4.x,Pytest 7.x。第一步先把虚拟环境建起来,避免污染系统 Python:

python -m venv venv source venv/bin/activate pip install pytest selenium webdriver-manager pytest-html allure-pytest

这里重点说一下浏览器驱动的问题。Selenium 本身只是一个驱动库,它需要配合浏览器对应的驱动程序才能控制浏览器。最常见的问题是 chromedriver 的版本必须和 Chrome 浏览器版本匹配,否则启动浏览器时直接报 session 创建失败。

以前处理这个问题的办法是去官网手动下载对应版本的驱动,放到 PATH 里。现在推荐用 webdriver-manager,它会在运行时自动检测本地 Chrome 版本,下载匹配的驱动,省掉很多手工维护工作。如果你在的公司网络受限,自动下载可能失败,那就需要提前把驱动下载好放到指定目录,然后通过 Service 指定路径加载。

我在工程里还顺手加了一个告诉团队成员的依赖说明文件,把 Python 版本、Chrome 版本、依赖库版本都写清楚。这个动作看起来不起眼,但它真的能帮同事省下不少排查时间。测试环境的一致性,是自动化脚本能稳定运行的隐形前提。

2.2 测试工程目录结构与分工

测试代码的组织方式,我直接采用经典的 Page Object Model 模式,这是目前 Web UI 自动化最主流的设计思路。核心思想是把页面元素和操作逻辑封装成独立的页面类,测试用例只关心业务操作,不关心元素定位细节。这样页面一旦改版,只需要修改对应的页面类,测试用例主体不受影响。

工程目录结构大致是这样的:

login_test/ ├── conftest.py # pytest 全局 fixture ├── requirements.txt # 依赖清单 ├── pages/ │ ├── __init__.py │ ├── base_page.py # 页面基类,封装通用操作 │ └── login_page.py # 登录页对象,封装元素定位和操作 ├── test_cases/ │ ├── __init__.py │ └── test_login.py # 登录测试用例 ├── data/ │ └── login_data.py # 测试数据,可用 Excel 或 YAML 替代 ├── utils/ │ ├── __init__.py │ └── screenshot.py # 失败截图工具 └── reports/ # 测试报告输出目录

这个结构的核心是分层清晰。pages 层管页面操作,test_cases 层管用例组织和数据调用,conftest.py 管共享的测试前置条件。对于登录模块这种页面数量少、用例相对集中的场景,这个结构完全够用。如果后续真的要扩展到整个产品线,还可以在这个基础上加业务层、数据工厂层,但现阶段没必要为了架构而架构。

2.3 conftest.py 的核心配置与 fixture 设计

conftest.py 是整个测试工程的底座,我在这里定义了三个关键 fixture:浏览器实例、登录页对象、测试数据。浏览器实例的 fixture 采用 function 作用域,保证每个用例都拿到一个全新的浏览器会话,用例之间互不影响。

import pytest from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.service import Service @pytest.fixture(scope="function") def driver(): service = Service(ChromeDriverManager().install()) options = webdriver.ChromeOptions() options.add_argument("--start-maximized") options.add_argument("--ignore-certificate-errors") driver = webdriver.Chrome(service=service, options=options) yield driver driver.quit()

这里有一个细节容易被忽略:为什么要把浏览器实例的 scope 设为 function 而不是 class 或 module?因为登录模块的用例彼此独立,如果用同一个浏览器实例连续跑多个用例,前一个用例留下的 cookie 和登录态会干扰后一个用例的初始状态。比如某个用例登录失败了,页面停留在登录页,下一个用例打开的时候可能还带着上一个用例的输入痕迹。用 function 作用域虽然慢一点,但是稳。稳定性和性能之间,测试脚本永远优先选稳定性。

3. 登录用例实现与关键技术点

3.1 页面对象封装与元素定位策略

页面对象封装的核心,是把元素定位和页面操作内聚到同一个类里。以登录页为例,通常会有用户名输入框、密码输入框、登录按钮、错误提示区域、首页的用户头像或昵称。把这些元素全部定义为类属性,再用方法表示用户行为。

from selenium.webdriver.common.by import By from pages.base_page import BasePage class LoginPage(BasePage): username_input = (By.CSS_SELECTOR, "input[name='username']") password_input = (By.CSS_SELECTOR, "input[name='password']") login_button = (By.CSS_SELECTOR, "button[type='submit']") error_toast = (By.CSS_SELECTOR, ".ant-message-error, .toast-error") def login(self, username, password): self.input_text(self.username_input, username) self.input_text(self.password_input, password) self.click(self.login_button) def get_error_message(self): element = self.wait_presence(self.error_toast) return element.text

元素定位策略上,我强烈建议优先用 CSS 选择器而不是 XPath。CSS 选择器在 Selenium 中的执行效率更高,语法也更简洁。只有在需要根据元素的文本内容定位、或者需要从兄弟节点、父节点反推目标元素时,才考虑用 XPath。

另外要注意避免使用动态生成的 id 或 class 属性。很多前端框架会在每次渲染时生成随机后缀,比如 id="login-btn_12345",这种属性今天能用明天就废了。我在项目里会推动前端团队在关键测试节点上加>from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class BasePage: def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10) def wait_clickable(self, locator): return self.wait.until(EC.element_to_be_clickable(locator)) def wait_visible(self, locator): return self.wait.until(EC.visibility_of_element_located(locator))

在登录场景里,有一个等待的坑特别典型:点击登录按钮之后,页面可能先跳转,再异步加载首页数据。如果脚本在点击之后立刻去断言首页元素,大概率会扑空。正确做法是等待首页的标志性元素出现,比如用户昵称、导航栏或者某个特有的页面标题,而不是去 sleep 几秒。

这背后体现的是一个重要的测试思维:永远不要假设时间,只验证条件。时间的快慢是环境决定的,条件满足与否才是业务逻辑决定的。

3.3 基于参数化的数据驱动用例设计

登录模块的典型特点是:同一个操作流程,不同的输入数据,期望不同的结果。这种场景和 Pytest 的参数化机制天然契合。用 @pytest.mark.parametrize 装饰器,把测试数据和预期结果组织成列表,每跑一组数据相当于执行一个独立的用例。这样既省代码量,又多跑出了覆盖度。

import pytest from pages.login_page import LoginPage @pytest.mark.parametrize("username,password,expected", [ ("test_user_001", "Passw0rd!", "login_success"), ("", "Passw0rd!", "empty_username"), ("test_user_001", "", "empty_password"), ("test_user_001", "wrong_password", "password_error"), ("not_exist_user", "Passw0rd!", "user_not_exist"), ]) def test_login_cases(driver, username, password, expected): page = LoginPage(driver) page.open_login_page() page.login(username, password) if expected == "login_success": assert page.is_login_success(), "登录成功后应跳转到首页" else: actual = page.get_error_message() assert actual, "应该出现错误提示信息"

这一段代码里,测试数据直接写在装饰器里,适合用例数量少的情况。后期如果数据量太多,可以把数据抽取到 data/login_data.py 或者 YAML 文件里,通过 fixture 加载。注意数据文件里的测试账号要和测试环境保持一致,尽量用测试库专用账号,不要碰生产账号,也不要用真实用户数据。

3.4 登录成功后的状态管理与用例隔离

登录用例之间最大的干扰因素就是登录态。一个用例通过登录之后,浏览器会保存 cookie,这些 cookie 会影响后续用例的初始状态。为了避免这个问题,我做了两层处理:

第一层,每个用例都从完全干净的状态开始,driver fixture 是 function 作用域,浏览器实例每次新建,cookie 天然隔离。第二层,登录成功后的断言只验证页面关键元素,不做多余操作。有些用例如果确实需要登录状态,比如验证登录后才能访问的页面,我不会把它和登录测试混在一起写,而是单独放到需要权限的模块里。

这里还要注意一个细节:清理浏览器上下文。即便每个用例都新建 driver,在某些情况下浏览器进程可能没有完全退出,导致资源占用过高。在 conftest.py 的 teardown 阶段,除了 driver.quit(),我还加了 driver.delete_all_cookies() 和 driver.clear_local_storage() 的尝试性清理,避免极少数场景下 cookie 没有及时失效。

4. 执行过程与问题排查实录

4.1 首次运行踩过的典型坑

这套脚本从写完到稳定运行,中间踩了不少坑。第一个坑是浏览器驱动版本不匹配。当时本地 Chrome 自动更新到了新版本,但环境变量里还是旧版 chromedriver,启动浏览器直接报 “session not created: This version of ChromeDriver only supports Chrome version xxx”。后来用 webdriver-manager 自动获取匹配版本,这个坑就彻底解决了。

第二个坑是元素定位超时。登录按钮的 class 属性里包含动态生成的字符串,看起来差不多,实际每次刷新都不一样,导致定位失败。排查了半天才发现问题不在代码逻辑,而是选择器本身不稳定。后来把定位改成相对稳定的层级结构,比如通过 form 内 button[type='submit'] 这样向上找所在的表单容器,问题立刻解决。

第三个坑是 iframe 和窗口切换。登录模块为了支持第三方扫码登录,页面里嵌了一个 iframe。第一次跑的时候 driver.find_element 怎么也找不到 iframe 里的元素,报错说元素不在当前上下文里。后来查看页面结构才发现目标元素在 iframe 内部,必须先 switch_to.frame 再操作里面的元素。这也是很多 Web 自动化新手会忽视的点。

4.2 验证码和滑块验证的处理思路

登录自动化里最让人头疼的,大概率是验证码和滑块验证。图形验证码无法直接识别,滑块验证又涉及拖拽轨迹模拟,这些都天然反自动化。我的处理思路不是去破解它们,而是从测试工程层面绕过去。

实际操作中,我会推动测试环境屏蔽验证码。具体做法有几种:测试环境配置一个开关,关闭验证码服务;或者预留一个万能验证码,只对测试环境生效;再或者调用后端接口获取验证码的内容,填到前端。最彻底的做法是登录接口层面直接打个令牌,然后把这个令牌注入到浏览器 cookie 中,绕开 UI 登录流程。这个方案适合需要登录态但本身不测登录逻辑的用例。

如果你确实要测带验证码的登录流程,那就必须在用例里预留人工介入步骤,配合 pytest 的暂停机制,让测试人员在验证码输入之后再继续跑。这种脚本虽然不能完全自动化,但它保证了主流程能跑、验证码环节可控,也算是一个折中方案。

4.3 常见问题速查表

我把跑登录自动化过程中遇到的高频问题整理成了表格,后面团队接手的时候可以照着排查:

问题现象可能原因解决方案
浏览器启动失败,报 session not createdchromedriver 与 Chrome 版本不匹配使用 webdriver-manager 自动匹配,或手动下载对应版本
元素定位超时使用了动态属性,或元素在 iframe 内更换稳定选择器,检查是否需要 switch_to.frame
元素报 not interactable元素被遮罩层遮挡,或处于禁用状态先等待元素可点击,必要时 scrollIntoView
点击登录按钮后结果不稳定点击太快,页面 JS 尚未绑定事件用 element_to_be_clickable 等待,或先点击其他区域触发绑定
用例之间登录态串了driver 作用域过大,cookie 未清理fixture 使用 function 作用域,执行前清理 cookie
登录成功断言偶尔失败首页元素加载较慢等待首页标志性元素出现,不要用固定 sleep
扫码登录无法自动处理依赖第三方手机 App测试环境关闭扫码,或采用 token 注入方案

这个表格看起来简单,但每一条都是实际测试踩坑换来的。建议你在自己的项目里遇到新问题,也顺手填进类似的表格,形成团队自己的问题库。

4.4 失败截图与报告集成

自动化测试的脚本挂掉不可怕,可怕的是挂掉之后没有任何现场信息。我强烈建议给每个用例加上失败截图机制。实现方式很简单,在 pytest 的 fixture teardown 阶段判断用例是否执行成功,如果失败则调用 driver.save_screenshot。

@pytest.hookimpl(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: screenshot_path = f"reports/failures/{item.name}_{int(time.time())}.png" driver.save_screenshot(screenshot_path)

报告方面,我用了两个方向:简单场景直接上 pytest-html,执行完之后会生成一个带测试用例明细的 HTML 报告;如果团队有集中展示需求,就配置 allure-pytest,Allure 的报告在展示维度上明显更丰富,可以按功能模块统计、查看每个用例的步骤和截图。

pytest -v -s --html=reports/login_report.html --alluredir=reports/allure-results allure generate reports/allure-results -o reports/allure-report --clean

跑完去打开报告看,一眼就能看出哪些用例挂了、为什么挂、失败瞬间的页面长什么样。这比看控制台日志高效太多。

4.5 并发执行与稳定性处理

登录用例数量不算多,但为了演示扩展开可以提一下并发。pytest-xdist 插件可以让多个用例并行执行,命令加上 -n 参数即可指定并发数。但并发执行有一个前提:用例之间必须绝对独立,不能共享任何状态。登录用例天然满足这个条件,因为每个用例都是新建浏览器、独立 cookie,所以配上 -n 4 能显著缩短整体执行时间。

不过我并不建议把所有 Web UI 用例都丢到并发里跑。浏览器实例本身很吃资源,并发数开太高容易把测试机卡死。实际项目里,我一般先串行跑两遍确认脚本稳定,再考虑并发优化。稳定第一,速度第二。

5. 经验总结与后续扩展

5.1 登录自动化的三层执行经验

写这套登录自动化,给我最大的体会是三层递进的关系:第一层是页面能打开、元素能定位、步骤能跑通,这属于“能跑”;第二层是数据分离、用例独立、异常能自动截图,这属于“稳定”;第三层是能和 CI 流程集成、报告自动发送、失败自动定位,这属于“好用”。大多数初学者的脚本停在第一层,跑一次行,跑十次就开始挂。而真正的自动化测试项目,追求的是第十次和第一次结果一致。

要跨过第一层到第二层,核心是做好等待策略和用例隔离。要跨过第二层到第三层,核心是做好结果反馈和报告沉淀。每一层都有不同的技术难点,但都是可以通过持续迭代逐步完善。

5.2 从 UI 测试走向接口与 UI 分层

登录自动化稳定跑了一段时间之后,我开始琢磨怎么让这套东西的效率更高。核心思路是分层:接口测试覆盖绝大多数异常分支,UI 测试只保留关键路径和交互验证部分。接口测试跑得快、维护成本低,UI 测试保留少量冒烟用例,确保真实用户路径可用。

具体操作上,我会用 Requests 或者 Pytest 的 fixtures 直接请求登录接口,验证返回的 token、错误码和用户信息。这些用例不依赖浏览器,跑起来秒级完成,可以覆盖密码错误、用户不存在、参数异常这些高频分支。而 UI 层只保留四条核心用例:正常登录、空用户名、空密码、错误密码提示。两层加起来,既保证了覆盖面,又把执行时间控制在了合理区间。

5.3 与 CI 流水线的集成方向

如果你的团队有持续集成环境,登录自动化完全可以做成流水线里的一个质量门禁。思路是将 pytest 执行命令、报告生成命令、甚至失败重跑机制都写进流水线脚本,每次代码合并前自动跑一遍登录冒烟用例。我在实际项目里用过两种方式:一种是 Jenkins 定时任务,每天凌晨跑全量用例,上班看报告;另一种是提交触发,只在登录模块相关文件变更时触发执行。

无论哪种方式,核心是让测试结果的反馈链路足够短。用例挂在流水线里,责任人第一时间收到通知,点开报告就能看到失败截图和堆栈信息。这样自动化测试的价值才真正释放出来,而不是变成一个“每周跑一次但没人看”的形式主义。

按我的实际经验,登录自动化跑起来之后,真正省时间的地方不在于你不用手动点那几下,而在于它逼你把登录模块的所有边界条件都梳理了一遍。我在写这套用例时最深的体会就是:元素定位和等待策略的可靠程度,决定了这套脚本到底能用一周还是能用一年。如果你也在写登录自动化,建议先别贪用例数量,优先把环境隔离和失败截图做扎实,后面加用例会顺手很多。

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

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

立即咨询