登录自动化实战:Playwright+Unittest+CSV+BeautifulReport 坦白说登录模块是我接手过的Web自动化项目里被问得最多的一个场景。它看起来简单无非是填用户名、填密码、点按钮、看跳转但真正落地成一套能长期跑的回归脚本时牵涉到的技术点其实一点不少用例怎么组织、浏览器怎么驱动、测试数据怎么管理、执行结果怎么让人一眼看懂。这也是为什么我会把 Unittest、Playwright、CSV 和 BeautifulReport 这四样组合在一起做一个完整闭环——它们各自解决一个环节合起来才是一套可持续维护的登录自动化模板。这篇就从头到尾拆一遍环境、代码、数据、报告、踩坑全给你捋清楚。1. 项目思路与技术选型解析1.1 登录自动化到底要解决什么问题先明确一点我们做登录自动化不是只为了“能自动点几下”。真正的目标是回归稳定、数据可控、结果可读。登录是绝大多数系统的入口每次版本迭代改登录页、改密码策略、加验证逻辑都要有人手工验证一遍这个重复劳动成本很高。自动化脚本的核心价值不是替代人而是把人从重复操作里解放出来去做更有判断力的事情。在这个项目里登录业务自动化要覆盖四类检查正常账号密码能否登录成功、错误密码能否被拦截、空账号空密码的提示是否友好、登录之后的跳转和页面元素是否符合预期。这四类场景如果全写在代码里后续业务改一条文案、改一个跳转地址你都要去改脚本维护成本极高。所以这里做了数据驱动的设计把测试输入和预期结果全部抽到 CSV 文件代码只负责“读数据—执行—断言—出报告”这个固定流程。这背后其实是分层的思想在起作用数据文件是一层页面对象是一层用例逻辑是一层报告输出是一层。哪一层出了问题只改那一层就行不用整个推翻。这个思路对于登录这种要素固定的模块来说尤其合适。1.2 为什么是 Playwright、Unittest、CSV、BeautifulReport 这套组合工具选型这事没有绝对的对错只有适不适合。我在这个项目里选择这四样是综合了成本、生态、维护性和团队熟悉度之后的结果。先看浏览器自动化这一层。Playwright 和 Selenium 的对比我用下面这张表概括对比维度PlaywrightSelenium说明浏览器驱动自带 install 命令自动下载管理浏览器内核需要手动下载对应版本的 chromedriver/geckodriver前者省掉了很多环境配置时间自动等待内置 Web-first 断言自动重试不用手动塞 sleep需要显式写 WebDriverWaitPlaywright 在等待这一块确实更省心多页面/多标签原生支持 Page/Context 隔离需要手动切换 handle代码啰嗦登录后开新标签的场景Playwright 写起来更顺元素定位locator 语义化定位支持 get_by_role/get_by_text以 CSS/XPath 为主新版 API 对前端开发者更友好社区成熟度快速上升期资料越来越多成熟多年历史资料最多老项目用 Selenium 合理新项目可以优先考虑 Playwright再看向上的一层。Unittest 是 Python 标准库自带的测试框架零依赖零成本团队里只要有 Python 基础就能直接上手。虽然 Pytest 在插件生态和 fixture 上有优势但 Unittest 胜在稳定和通用尤其在企业环境里装第三方包还要走审批流程的话Unittest 是最稳妥的选择。然后是数据层。CSV 和 Excel、数据库、YAML 相比最大优势是轻量和好维护。Excel 可以折腾样式但每次运行都要依赖 Excel 组件解析跨平台容易出问题YAML 虽然好看但对不懂代码的同事来说改个缩进就可能整个文件解析失败。CSV 是纯文本记事本能开、Excel 能开、版本管理工具能直接 diff非常适合做“业务人员只需要改用例数据”的协作模式。最后是报告层。BeautifulReport 是 unittest 生态里一个开箱即用的 HTML 报告库样式简洁统计信息一目了然失败用例能直接看 traceback 和截图日常回归够用了。相比 Allure不需要额外安装命令行工具和服务对中小项目来说更轻。2. 环境准备与项目初始化2.1 搭建干净的 Python 运行环境我建议所有自动化项目都从虚拟环境开始别直接往系统 Python 里装包。不同项目依赖不同版本今天装一个 playwright明天装那个 selenium时间长了全局环境会乱到你想重装系统。创建虚拟环境的操作很简单python -m venv web_auto_envWindows 下激活web_auto_env\Scripts\activatemacOS 或 Linux 下激活source web_auto_env/bin/activate激活之后命令行提示符前面会出现(web_auto_env)这代表你现在用的是隔离环境。接下来所有安装和执行操作都在这个环境里进行。这一步的意义在于项目可复制换台机器拉代码、建环境、装依赖几分钟就能跑起来而不是在本机跑得好好的换台机器各种缺包报错。2.2 安装依赖和浏览器内核依赖只有两个核心包一个playwright一个beautifulreportUnittest 是标准库不用装。pip install playwright pip install beautifulreport装完 playwright 包之后还需要下载浏览器内核。这一步很多人会卡住常见报错是下载到一半超时。Windows 下建议先设置一个环境变量再执行$env:PLAYWRIGHT_DOWNLOAD_HOST https://npmmirror.com/mirrors/playwright playwright install chromiummacOS 或 Linux 是export PLAYWRIGHT_DOWNLOAD_HOSThttps://npmmirror.com/mirrors/playwright playwright install chromium实测下来这个配置最稳能直接避开下载内核阶段的各种超时问题。装完之后可以跑一句验证命令playwright codegen https://example.com如果浏览器能正常弹出来并且右侧出现录制窗口说明环境就绪了。codegen这个工具强烈推荐它能录制操作并自动生成定位器代码做登录脚本时先录一轮能省掉不少手动找选择器的时间。2.3 项目目录结构怎么分目录结构我遵循惯例按职责分层web_auto_login/ ├── main.py # 执行入口负责收集用例和生成报告 ├── requirements.txt # 依赖清单 ├── common/ │ └── csv_reader.py # CSV 读取工具 ├── pages/ │ └── login_page.py # 登录页面对象 ├── testcases/ │ ├── __init__.py │ └── test_login.py # 登录测试用例 ├── data/ │ └── login_data.csv # 测试数据 └── report/ # 报告输出目录 └── screenshot/ # 失败截图目录这样的分层好处很明显业务同事只动data/login_data.csv用例维护者只改testcases/下的文件页面变了只碰pages/公共方法统一放common/。这种分工恰恰是前面说的“闭环”能持续运转的组织基础——不是所有修改都要找写脚本的人。3. 登录页面对象封装与定位策略3.1 Page Object 模式解决的核心痛点登录脚本如果直接在用例里写page.locator(...).click()短时间跑通没问题但一个页面被十个用例引用每个用例里都复制一套定位表达式等页面结构调整你就知道痛了十处代码要一起改漏一处就失败一次。Page Object 模式POM把页面上的关键元素和操作封装成一个类用例层只跟这个类的行为方法打交道不关心底层用的是 CSS 选择器还是 XPath。这就像你去餐厅吃饭只需要对着菜单点菜不用管后厨用什么锅炒菜。前端改了元素结构我唯一要改的地方是页面对象类用例代码一动不动。3.2 登录页面对象示例下面是一个结构清晰的登录页对象封装只暴露外部需要的动作和状态from playwright.sync_api import Page, expect class LoginPage: def __init__(self, page: Page): self.page page # 定位器统一收敛在这里 self.username_input page.locator(#username) self.password_input page.locator(#password) self.login_button page.get_by_role(button, name登录) self.error_message page.locator(.login-error) def goto_login_page(self, url: str): self.page.goto(url) def login(self, username: str, password: str): self.username_input.fill(username) self.password_input.fill(password) self.login_button.click() def get_error_text(self) - str: return self.error_message.inner_text() def wait_for_login_success(self, expected_url_part: str): expect(self.page).to_have_url(re.compile(f.*{expected_url_part}.*))这里我选择了get_by_role(button, name登录)而不是page.locator(button.submit-btn)是因为按钮的语义角色比 CSS 类名更稳定。很多前端重构只改样式类不动文案和按钮语义这种情况下角色定位的脚本能少挂好几次。3.3 定位策略与等待机制Playwright 的自动等待是它最大的省心点之一。fill()和click()在执行前会自动等待元素出现在 DOM 中且可操作这帮我消灭了大量原来 Selenium 时代最头疼的元素未找到问题。但自动等待不等于万能有几种情况还是得手动处理登录后跳转需要时间断言 URL 时必须用expect(page).to_have_url(...)不能用time.sleep()。元素虽然在 DOM 里但被遮罩层挡住可以先调用scroll_into_view_if_needed()再用click()。错误提示是动态渲染的最好先locator.wait_for(statevisible)再取文本。一个常见的坑是有些人写 Playwright 脚本时还带着 Selenium 的习惯动不动time.sleep(2)一把梭。这样做的后果是脚本跑得慢而且网络好一点差一点都能导致不稳定。Playwright 的expect自带重试机制默认超时 5 秒基本上元素只要在超时时间内出现脚本就继续走比固定 sleep 可靠得多。4. CSV 数据驱动模块实现4.1 测试数据文件如何设计登录业务的数据文件我建议至少包含这些字段字段示例值含义case_idlogin_001用例编号报告里定位问题用usernamestandard_user登录用户名passwordTest123456登录密码expected_url_partdashboard登录成功后的 URL 关键字expected_message用户名或密码错误登录失败的预期提示remark正常用户登录备注方便业务同事理解用 Excel 打开 CSV 维护时记住选择“UTF-8 编码”保存不要把文件存成 ANSI 或者 GB18030否则脚本读取中文预期文案时容易出现乱码。我自己的习惯是统一用 UTF-8 保存读取时也明确指定编码。如果用的是 WPS 表格另存为 CSV 时同样要留意编码选项选 UTF-8 格式的 CSV 导出。4.2 读取 CSV 的工具类写法CSV 读取模块要做得简单、健壮能直接转换成列表字典方便调用import csv from pathlib import Path def read_csv_data(csv_path: str) - list[dict]: abs_path Path(__file__).parent.parent / csv_path rows [] with open(abs_path, moder, encodingutf-8-sig, newline) as file: reader csv.DictReader(file) for row in reader: rows.append({key.strip(): value.strip() for key, value in row.items()}) return rows这里有两个细节值得说。第一encodingutf-8-sig而不是utf-8。用 Windows 的 Excel 保存 UTF-8 CSV 时文件开头会带一个 BOM 头\ufeff直接按 utf-8 读取会导致第一个字段名多出一个不可见字符你用row[username]时就会 KeyError。utf-8-sig打开时会自动把 BOM 去掉这个坑我踩过不止一次。第二newline可以避免 CSV 读取时出现多余空行也是 Python 处理 CSV 的标准做法。4.3 数据驱动与 Unittest 用例的绑定方式有了数据之后接下来是把 CSV 里的每一行数据变成一个可执行的测试用例。这里有两种做法。一种是快速实现方案在用例方法内部循环数据用subTest区分import unittest from common.csv_reader import read_csv_data class TestLogin(unittest.TestCase): def test_login_with_csv_data(self): dataset read_csv_data(data/login_data.csv) for data in dataset: with self.subTest(casedata[case_id]): self.run_login_case(data)这种方案代码少缺点是 BeautifulReport 报告里整个test_login_with_csv_data只显示一个用例哪条数据挂了只能在失败详情里看case_id不够直观。另一种是可维护方案在导入测试类的时候动态生成每个数据对应的测试方法import unittest from common.csv_reader import read_csv_data from pages.login_page import LoginPage from playwright.sync_api import sync_playwright class TestLogin(unittest.TestCase): base_url https://example.com/login classmethod def setUpClass(cls): cls.playwright sync_playwright().start() cls.browser cls.playwright.chromium.launch(headlessTrue) classmethod def tearDownClass(cls): cls.browser.close() cls.playwright.stop() def run_login_case(self, data: dict): context self.browser.new_context() page context.new_page() try: login_page LoginPage(page) login_page.goto_login_page(self.base_url) login_page.login(data[username], data[password]) if data[expected_url_part]: login_page.wait_for_login_success(data[expected_url_part]) else: error_text login_page.get_error_text() self.assertIn(data[expected_message], error_text) finally: context.close() def create_login_case(data: dict): def test_case(self): self.run_login_case(data) return test_case for _idx, _row in enumerate(read_csv_data(data/login_data.csv)): test_name ftest_login_{_idx}_{_row[case_id]} setattr(TestLogin, test_name, create_login_case(_row))这种做法的好处是报告里每一行 CSV 数据都对应一个独立用例看报告时能直接知道login_003这条数据挂了、挂在了哪一步。后续如果要筛选用例也能按方法名做更灵活的标记和过滤。我个人更推荐动态生成方案尽管它初看有点绕但报告的可读性和后续排查问题的效率是实打实的提升。5. 用例执行流程与浏览器生命周期管理5.1 Unittest 里的浏览器启动和回收策略浏览器资源属于重资源不能在每条用例里都启动一个浏览器进程那会慢到怀疑人生。我的管理策略是三层浏览器进程在setUpClass启动tearDownClass关闭整个测试类只启动一次。浏览器上下文每条用例单独创建new_context()模拟一个独立的全新会话。页面每个 context 下创建一个页面用完即关。context 这个概念要重点说一下它相当于一个独立的匿名会话环境隔离 cookie、缓存、localStorage。登录业务最大的坑之一就是用例之间相互串登录状态。如果不隔离 context第一条用例登录成功后第二条用例打开页面发现还是登录状态整个脚本的预期就全乱了。用 context 隔离之后每条用例都是干净状态这也是 Playwright 设计得比 Selenium 贴心的一个点。5.2 一组完整的登录用例执行逻辑下面这段代码展示的是核心的执行流程包含了成功和失败两类场景的断言分支import re import unittest from playwright.sync_api import sync_playwright from pages.login_page import LoginPage class TestLoginFlow(unittest.TestCase): base_url https://example.com/login classmethod def setUpClass(cls): cls.playwright sync_playwright().start() cls.browser cls.playwright.chromium.launch(headlessTrue) # 调试时可改为 False classmethod def tearDownClass(cls): cls.browser.close() cls.playwright.stop() def perform_login(self, username, password, expected_url_part, expected_message): context self.browser.new_context() page context.new_page() login_page LoginPage(page) try: login_page.goto_login_page(self.base_url) login_page.login(username, password) if expected_url_part: # 成功用例URL 包含预期的路径关键字 login_page.wait_for_login_success(expected_url_part) assert expected_url_part in page.url, fURL 跳转异常: {page.url} else: # 失败用例页面出现预期错误提示 error_text login_page.get_error_text() self.assertIn(expected_message, error_text) finally: context.close() def build_login_test_case(row): def test_case(self): self.perform_login( row[username], row[password], row.get(expected_url_part, ), row.get(expected_message, ) ) return test_case把这个方法和上一节的动态生成思路组合起来数据、用例、页面对象、浏览器管理层就全部打通了。headlessTrue用于 CI 和后半夜的定时回归本地调试时改成False可以看到浏览器界面方便观察每一步操作。5.3 失败截图和现场保留用例失败之后如果能保留一张现场截图排查效率会高很多。我的做法是在用例异常时自动截图把文件存到报告目录下并记录到 BeautifulReport 里。def save_screenshot(page, file_name): screenshot_path freport/screenshot/{file_name}.png page.screenshot(pathscreenshot_path, full_pageTrue) return screenshot_path调用时机放在except Exception里截图之后再把原始异常重新抛出保证不入库。这一步需要配合报告层的add_test_img方法具体在下一节说。这里有个经验截图要用full_pageTrue尤其是登录失败的提示信息可能在页面底部只拍视口的话容易把关键文案漏掉。6. BeautifulReport 报告集成与闭环打通6.1 为什么选 BeautifulReport报告是自动化闭环里最容易被忽视、但实际影响最大的一环。没有好报告脚本跑完你还是要自己去翻日志、猜结果有了清晰的报告业务同事和领导打开网页一眼就能看到今天登录回归有没有挂、挂了几条、挂在什么地方。我之前用 HTMLTestRunner 出的报告样式老、信息挤成一团失败和成功基本全靠肉眼扫描。BeautifulReport 解决了这个痛点它是中文界面包含用例总数、通过率、执行时间、失败/错误明细还能关联失败截图作为登录回归的展示层非常合适。当然 Allure 功能更强但为了用它的酷炫效果你需要额外安装 Allure 命令行、维护历史记录这些成本对一个几十条用例的登录项目来说偏重了。BeautifulReport 用一个 pip 包解决报告问题简单直接。6.2 主入口和报告生成代码main.py是整个项目的聚合入口负责发现用例、执行、生成报告import unittest from BeautifulReport import BeautifulReport if __name__ __main__: suite unittest.defaultTestLoader.discover(testcases, patterntest_*.py) report BeautifulReport(suite) report.report( filenamelogin_auto_report, description登录业务自动化回归报告, log_pathreport )执行之后report/目录下会生成一个login_auto_report.html文件。浏览器打开就能看到用例总数和通过率统计每条用例名称和对应的耗时失败用例的错误堆栈信息通过率环形图如果测试类里加了__test__的docstring报告里会显示这些描述信息方便业务人员快速理解用例意图。失败截图关联这块的写法是# 在测试类中捕获异常后 from BeautifulReport import BeautifulReport class TestLoginFlow(unittest.TestCase): ... # 异常处理时调用 BeautifulReport.add_test_img(report/screenshot/login_failure.png)这样报告失败详情区域会直接内嵌显示截图排查问题的时候不用再去翻文件目录。6.3 全流程闭环怎么串起来到这里整个完整闭环已经成型业务同事在data/login_data.csv增删改测试数据。执行者运行python main.py。Playwright 按 CSV 数据逐条执行登录操作和断言。执行结果由 BeautifulReport 汇总成 HTML 报告。运维/测试人员查看报告失败项定位到具体数据和步骤提交缺陷修复。修复后再次执行main.py回归验证闭环。这套流程可以进一步接上 Windows 任务计划或 Jenkins 定时构建实现每天凌晨自动跑一遍登录回归。接入的方式很简单定时任务命令行执行python main.py即可。如果使用 Jenkins可以增加一个构建后步骤归档report/*.html每次构建完点击就能打开报告。7. 高频问题与避坑速查表7.1 环境安装和浏览器启动相关错误现象原因解决方案ModuleNotFoundError: No module named playwrightPython 环境里没装包或装在了别的环境确认虚拟环境已激活执行pip install playwrightExecutable doesnt exist at ...已装包但没下载浏览器内核执行playwright install chromium下载浏览器内核超时、卡在 99%本地网络下载受限设置PLAYWRIGHT_DOWNLOAD_HOST为国内镜像源后重新安装Chromium 启动失败提示沙箱问题Linux 系统缺少依赖库安装系统依赖playwright install-deps chromiuminstall-deps这个命令在 Linux 服务器上比较有用它会自动安装浏览器运行所需的系统库很多 Jenkins 节点上跑不起来的问题就靠这一句解决。7.2 定位和操作相关错误现象原因解决方案点击登录按钮无反应脚本不报错按钮被遮罩层挡住或按钮不可点用locator.scroll_into_view_if_needed()滚动到可见区域再click()expect(page).to_have_url()超时登录成功后跳转逻辑较慢调大expect的timeout参数例如timeout10000输入框fill()成功但值没生效页面元素被前端框架重复渲染改用press_sequentially()模拟键盘输入错误提示文本取不到提示是异步加载的出现较晚先wait_for(statevisible)再取inner_text()一个容易被忽略的点是前端框架如果是 Vue 或 React输入框值可能绑定在受控组件上直接fill()虽然能填进去但触发的 input 事件可能不是框架监听的那一个。遇到这种情况press_sequentially会更接近真实用户的输入行为。7.3 数据与业务场景相关错误现象原因解决方案CSV 中文字段读取后乱码文件编码不是 UTF-8 或读取时没指定编码统一保存为 UTF-8读取用utf-8-sig多条用例同时执行后一条被强制登出同一账号被多会话并发登录互踢每条用例独立测试账号或将执行方式改为串行登录成功后页面出现验证码/滑块拦截风控逻辑在测试环境也触发测试环境提前关闭验证码开关或单独处理风控逻辑用例在本地通过Jenkins 上总失败CI 节点无浏览器窗口环境屏幕尺寸默认过小headless 模式运行并手动设置 viewport 尺寸账号互踢这个问题在登录自动化里非常典型。如果团队只有一两个测试账号建议在数据文件里给每条用例配不同账号或者排队串行执行避免同时验证一个会话。7.4 报告相关问题错误现象原因解决方案报告文件名生成了但内容是空的用例发现路径不对发现的用例数为 0确认discover的 pattern 参数和testcases目录路径正确报告里失败详情没有截图截图没保存到正确路径确认截图已生成并在失败处理分支里调用add_test_img报告中文显示乱码HTML 模板默认编码问题确认 Python 3 环境用新版 beautifulreport 包我个人在实际运营这套登录自动化的过程中最深的一个体会是框架选型只是起点真正决定项目成不成的是“数据、脚本、报告”这三个环节是否真的分开。把测试数据还给业务、把页面结构隔离给页面对象、把报告做清楚给管理者整个流程运行起来才顺畅。后面如果需要继续扩展可以考虑把main.py入口升级成命令行工具支持指定环境地址、指定 CSV 数据文件、指定要跑的浏览器类型chromium、firefox、webkit这样对接 Jenkins 或者做多环境冒烟测试都会灵活很多。