☰
Selenium+unittest实战:CRM系统自动化测试项目源码全解析
2026/10/13 14:30:12 网站建设 项目流程

简介:CRM系统项目实战源码是一套完整的Web自动化测试实战资料,主要面向软件测试工程师、自动化测试初学者以及想要提升框架搭建能力的Python开发者。这份资源带着学习者从CRM业务场景出发,逐步完成Selenium自动化脚本、unittest测试框架的组织与运行、测试报告生成以及邮件自动发送,整个流程覆盖了Web自动化测试从入门到落地的核心环节,也清晰展示了自动化测试框架应该怎样设计、各类文件怎样分工配合。压缩包内共包含254个文件,大小约24.19MB,其中以Python源码文件为主,同时有编译后的pyc文件、HTML报告、yaml配置文件、日志记录以及样例数据等,目录结构比较规整,便于按模块查找和二次修改。目前已有222人参与学习下载,适合用来做自动化测试项目练手、框架设计参考或者毕业设计选题,能够帮助读者快速建立对Web自动化测试全流程的直观认识。

1. 这套 CRM 自动化测试项目到底在做什么:从一条回归用例说起

你接手过那种每次发版都要手工点一遍的 CRM 系统吗?新增客户、改状态、查订单、删记录,二十几条用例点下来一上午就没了,还总有漏网之鱼。标题里的「CRM系统项目实战(源码)」就是指用 Selenium 驱动真实浏览器,把 CRM 的核心业务路径全部自动化,再用 unittest 组织测试用例,跑完自动生成一份 HTML 报告并通过邮件发给团队。这里说的 unitest 其实就是 Python 自带的标准测试框架 unittest,很多人写习惯了少打个字母。这套方案尤其适合两类人:一类是功能测试想转测试开发,另一类是已经在用 Selenium 写脚本,但用例一多就不知道怎么组织、报告和通知全靠人肉的操作者。它能解决的不只是省时间,而是把回归变成一条命令的事,并且让结果自动送到你眼前。

2. 环境准备与项目结构:把 Selenium + unittest 的底座搭稳

2.1 依赖安装与浏览器驱动:两个最容易翻车的点

做 Selenium 自动化,最基础的环境依赖就三个:Selenium 库本身、浏览器驱动、以及一个顺手的数据驱动库。Python 3.10 以上的环境里,我习惯用 pip 一次性装好:

pip install selenium ddt pip install webdriver-manager

解释一下为什么加webdriver-manager。如果不用它,你得自己去 chromedriver 官网下载和本地 Chrome 版本刚好匹配的驱动文件,然后手动指定路径,系统一更新就得重新换。用 webdriver-manager 后,可以用一行代码自动处理驱动下载和版本匹配:

from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager driver = webdriver.Chrome(ChromeDriverManager().install())

这里传给 Chrome 的ChromeDriverManager().install()会去缓存目录里找驱动,找不到就自动下载,省去手工维护。但注意,第一次运行会拉取驱动的版本元数据,如果公司网络对 Chromium 官方域名有限制,这步会超时。我的习惯是先在本地跑通一次,把驱动缓存好,再放到 CI 机器上,这样反而更稳定。

至于ddt,它是给 unittest 加数据驱动能力的三方库,后面第三节会重点讲。如果只靠 unittest 自带的subTest也能做,但代码表现力差一点,团队协作时不如下面这种写法直观。

2.2 项目目录怎么摆:按页面对象(POM)拆分还是平铺?

很多半路出家的自动化项目只有一堆test_login.py、test_customer.py平铺在同一个目录里,脚本一多就有三宗罪:元素定位字符串满天飞、公共操作复制粘贴、一个用例失败了其他用例跟着受影响。我建议按经典的 Page Object Model(POM)来组织,目录大概长这样:

crm_auto_test/ ├── pages/ # 页面对象层 │ ├── base_page.py │ ├── login_page.py │ └── customer_page.py ├── testcases/ # 测试用例层 │ ├── test_login.py │ ├── test_customer.py │ └── test_data.py ├── reports/ # 测试报告输出目录 ├── logs/ # 运行日志目录 ├── utils/ │ ├── driver_factory.py │ ├── report_util.py │ └── mail_util.py ├── config.py # 全局配置,如 base_url、等待时间 └── run_all.py # 执行入口,负责跑用例、生成报告、发邮件

页面对象层把每个页面的元素定位和操作封装成方法,比如LoginPage的login(username, password);测试用例层只关心业务操作和断言;run_all.py作为统一入口,不参与具体用例逻辑。这样分层的最大好处是:当 CRM 页面改了一个按钮的 class 或者 id,你只需要改对应 page 类里的定位,用例层几乎不动。如果测试脚本里仍然直接混着driver.find_element(...),那页面一改就要全局搜替换。

2.3 最小编码:一个能跑通的登录测试用例

先别想太复杂,第一步是让一个用例跑起来。下面这段是登录 CRM 的用例,注意我故意把浏览器初始化放在setUp里,关闭放在tearDown里,这样每个用例都是独立的浏览器会话,互不残留登录态。

import unittest from selenium import webdriver from config import BASE_URL, USERNAME, PASSWORD class TestLogin(unittest.TestCase): def setUp(self): self.driver = webdriver.Chrome() self.driver.maximize_window() self.driver.implicitly_wait(10) def tearDown(self): self.driver.quit() def test_login_with_valid_credentials(self): self.driver.get(BASE_URL + "/login") self.driver.find_element("id", "username").send_keys(USERNAME) self.driver.find_element("id", "password").send_keys(PASSWORD) self.driver.find_element("css selector", "button[type='submit']").click() # 登录成功后应跳转到首页,左上角有“工作台”三个字 self.assertTrue("工作台" in self.driver.page_source) if __name__ == "__main__": unittest.main()

这段代码的逻辑很清楚:setUp创建全新浏览器,get打开登录页,用find_element定位用户框和密码框输入,点击提交按钮,最后断言页面源码里出现了「工作台」这个关键字。注意find_element的第一个参数是定位策略字符串,支持"id"、"xpath"、"css selector"等;如果你用的 Selenium 版本比较老,可能得改成By.ID那种写法,但 4.x 以上都支持这种简写。implicitly_wait(10)的意思是:如果元素没立即出现,最多轮询等待 10 秒。这是最基础的兜底等待,但只对全局生效,后面会遇到更复杂的动态加载场景,不能用它替代显式等待。这一条用例跑通之后,你才有资格谈后面的数据驱动和报告。

3. 面向 CRM 业务的数据驱动用例:从登录到客户管理的必经路径

3.1 用 ddt 还是 unittest subTest?数据驱动选型

CRM 项目里最典型的数据驱动场景是表单校验:同一套新增客户的流程,要换不同的客户名称、联系人、电话、所属行业,跑完所有非法输入和合法输入的组合。如果用 unittest 原生写法,一个用例类里写 five 个 test 方法,每个方法里面都是几乎一样的代码,改一个定位就得同步改五个地方,这种无聊事我踩过太多次坑。更好用的是 ddt 库:

import unittest from ddt import ddt, data, unpack from pages.customer_page import CustomerPage @ddt class TestCustomerCreate(unittest.TestCase): """新增客户的数据驱动用例""" def setUp(self): # 这里复用登录逻辑,略 self.customer_page = CustomerPage(self.driver) @data( ("测试科技有限公司", "张三", "13800001111", "IT"), ("某贸易有限公司", "李四", "13900002222", "贸易"), ("", "", "", ""), # 全空,预期失败 ) @unpack def test_create_customer(self, name, contact, phone, industry): result = self.customer_page.create(name=name, contact=contact, phone=phone, industry=industry) if name: self.assertTrue("保存成功" in self.driver.page_source) else: self.assertTrue("请填写客户名称" in self.driver.page_source)

@ddt装饰测试类,@data传入一组一组的数据,@unpack负责把这组元组按位置参数展开传给测试方法。这样每条数据都相当于一个独立子用例,执行完之后报告里能分别看到每一条的通过/失败,而不是一整团灰色的大用例。和 unittest 自带的subTest相比,ddt 生成的测试名称更可读,比如test_create_customer_001,而 subTest 只显示一个用例内部循环,报告可读性差很多。如果你只跑一条数据就能满足,用 subTest 够了;只要涉及表单校验这类批量场景,直接上 ddt。

3.2 CRM 核心模块用例设计:客户新增、编辑、查询、删除

CRM 核心业务绕不开「增删改查」四个人,我的习惯是每个模块建一个独立的测试类,并用测试前缀控制执行优先级。因为 unittest 默认按 ASCII 码顺序执行用例,所以方法名可以直接设计成test_01_...、test_02_...,让新增跑在查询之前,查询跑在删除之前。但注意,这种依赖同一条测试数据的做法是脆弱的——如果新增失败了,后面的查询和删除因为找不到记录也会失败,导致报告里一片红。这时候我一般会加一个setUpClass级别的公共数据准备,用classmethod只执行一次,保证整个测试类共享一条当前测试数据。

import unittest from ddt import ddt, data, unpack from pages.customer_page import CustomerPage @ddt class TestCustomerModule(unittest.TestCase): """客户模块全链路:新增-查询-编辑-删除""" @classmethod def setUpClass(cls): cls.driver = webdriver.Chrome() cls.driver.implicitly_wait(10) # 这里调用公共登录方法 cls.customer_page = CustomerPage(cls.driver) @classmethod def tearDownClass(cls): cls.driver.quit() def test_01_create_customer(self): self.customer_page.create("某测试公司", "王五", "13700003333", "互联网") self.assertTrue(self.customer_page.check_toast("保存成功")) def test_02_search_customer(self): self.customer_page.search("某测试公司") rows = self.customer_page.get_table_rows() self.assertEqual(rows, 1) def test_03_edit_customer(self): self.customer_page.edit("某测试公司", {"联系人": "赵六"}) self.assertTrue(self.customer_page.check_row_contains("赵六")) def test_04_delete_customer(self): self.customer_page.delete("某测试公司") self.assertEqual(self.customer_page.get_table_rows(), 0)

这里有几个 CRM 自动化里必然会遇到的操作点:第一,toast提示是异步出现的,不能马上断言页面源码,应该用显式等待;第二,表格行数需要写一个稳定的获取方式,比如find_elements按 CSS class 拿到所有行对象;第三,编辑往往不是直接点按钮,而是先选中某一行,再点行内的「编辑」链接,此时要用xpath结合文本来定位。这些都是 CRM 页面常见的动态结构,我们下一节专门说等待和定位。

3.3 等待策略与动态元素定位:在表格和弹窗里找到对的元素

CRM 表格加载有快有慢,按钮点击之后的弹窗也是异步渲染。新手最容易在find_element上直接翻车,报NoSuchElementException。我刚入门时也被这种玄学搞得很头大,后来规则很简单:能用显式等待就不用隐式等待;能用 text 定位就不用索引定位;能用 XPath 结合上下文就不写一长串不落地的绝对路径。

看下面两个常用等待的用法:

from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 等待弹窗出现且“确认删除”按钮可点击 WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.XPATH, "//div[contains(@class,'modal')]//span[text()='确认删除']")) ) # 等待表格内包含“某测试公司”的行出现 WebDriverWait(self.driver, 10).until( EC.visibility_of_element_located( (By.XPATH, "//table//tr[contains(.,'某测试公司')]") ) )

第一段代码是标准弹出框确认按钮的等待方式,条件是元素不仅存在而且可点击,能有效避开按钮还处于禁用态的时间窗口。第二段代码是表格场景里最常用的策略:用 XPath 的contains(., '某测试公司')在整行文本里模糊匹配,不管这个名称出现在哪个单元格都能命中。等到了元素,实际点击时可能还是不稳定,因为深层 shadow DOM 或者 element 覆盖物,这种情况就要用ActionChains或者 JS 点击,后面进了进阶技巧再展开。

定位时还有一个常见误区:拿到了一串像这样的 XPath:

/html/body/div[2]/table/tbody/tr[3]/td[2]/a

它是从根节点一算到底的绝对路径,只要页面多一个 div、少一列表格,这个路径立刻废掉。正确做法是尽量用相对定位,比如「找到文本是编辑的那一行的链接」,这样写:

self.driver.find_element( By.XPATH, "//tr[contains(.,'某测试公司')]//a[contains(text(),'编辑')]" )

先通过行的文本内容锁定行,再在行范围内找「编辑」链接,这样即使表格结构微调,只要那行文字还在,定位就不会断。这就是我在 CRM 项目里用的最主要的「局部相对定位」概念。

4. 测试报告生成与邮件发送:报告不落地,等于没跑

4.1 用 HTMLTestRunner 还是 BeautifulReport?报告组件对比

unittest 原生输出的是控制台文本,看一眼还行,要发给测试组长和研发看?完全没意义。你得把结果汇总成一份带统计、带进度、带失败截图的 HTML 报告。最常见的两个组件是HTMLTestRunner和BeautifulReport。前者老牌,很多项目在用,但需要注意它对 Python3 并不完全兼容;后者是后来者,样式更现代,也自带截图、日志展示,github 上不少测试团队推荐。

我实际用下来的对比:

对比维度HTMLTestRunnerBeautifulReport
安装方式下载 .py 文件手动 importpip install BeautifulReport
报告样式经典表格,简洁卡片式,颜色直观
失败详情只给 traceback可带日志和截图
Python3 兼容需要改代码或换分支原生兼容
二次开发源码老,改动多结构清楚,可加自定义字段

如果你只想要一个最小可靠的方案,我建议直接用 HTMLTestRunner 的 py3 兼容版,网上有很多修好的版本,把它放进utils目录然后from utils.HTMLTestRunner import HTMLTestRunner就行。要是项目里有更高展示需求,趁早切换到 BeautifulReport,省得以后换。

报告生成其实很简单,核心代码长这样:

import unittest from utils.HTMLTestRunner import HTMLTestRunner suite = unittest.defaultTestLoader.discover(start_dir="./testcases", pattern="test_*.py") with open("./reports/crm_test_report.html", "wb") as f: runner = HTMLTestRunner( stream=f, title="CRM 自动化回归报告", description="用例执行统计", verbosity=2, ) runner.run(suite)

defaultTestLoader.discover会自动查找testcases目录下所有test_*.py文件里的测试类,拼装成 suite。HTMLTestRunner的stream是指输出文件对象,必须用二进制模式写;title和description会显示在报告头部;verbosity=2表示每个用例的详细执行结果都会输出到报告里。生成之后直接打开 HTML 文件就能看到成功数、失败数、错误数、耗时等统计信息。

4.2 邮件发送的三种实现:SMTP、附件、内嵌图片

报告生成了,如果只存在服务器上,还是要别人自己去拿。所以再加一个mail_util.py,专门负责把报告作为附件发出。这里有三种实现:第一种是最简单的附件发送;第二种是邮件正文写统计摘要;第三种是把报告里的截图内嵌到邮件正文,做出来花时间但效果很直观。我一般采取前两种组合,因为大多数团队只需要一个大致结论和附件。

看下面这段完整可用的发送代码:

import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.application import MIMEApplication from config import SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD def send_report(report_path, to_addrs): msg = MIMEMultipart() msg["From"] = SMTP_USER msg["To"] = ",".join(to_addrs) msg["Subject"] = "CRM 自动化测试报告" body = """ 你好,附件是本次 CRM 自动化回归测试报告,请查阅。 若有失败用例,请打开报告查看详细失败信息。 """ msg.attach(MIMEText(body, "plain", "utf-8")) with open(report_path, "rb") as f: part = MIMEApplication(f.read()) part.add_header("Content-Disposition", "attachment", filename=("utf-8", "", "crm_test_report.html")) msg.attach(part) with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT) as server: server.login(SMTP_USER, SMTP_PASSWORD) server.sendmail(SMTP_USER, to_addrs, msg.as_string())

这段代码的逻辑是:先构造一个MIMEMultipart对象作为容器,设置发件人、收件人、标题,再挂上纯文本正文和附件。附件部分用MIMEApplication读取 HTML 文件字节流,add_header里的filename用三元组("utf-8", "", "crm_test_report.html")是为了避免中文文件名乱码,如果文件名是英文写成filename="crm_test_report.html"也行。最后用SMTP_SSL连接邮箱服务器,登录后发送。如果你的企业邮箱协议是 STARTTLS 而非 SSL,就改成 smtplib.SMTP + server.starttls(),否则连接会被拒。

4.3 定时执行与结果通知:把报告推到钉钉/企业微信?但我习惯邮件

报告生成后,你还需要让这套流程定时跑起来。常见做法是用 CI 工具,比如某 CI 平台里配置定时构建,也可以直接在 Windows 任务计划或 Linux crontab 里执行python run_all.py。跑完拿到邮件就够了,不用每个人都去点开 CI 看日志。邮件本身就是一个老式的、但最不容易漏的通知渠道。如果你团队沟通工具是钉钉或企业微信,也可以在发邮件的同时用 Webhook 推一条消息,方案是在run_all.py里解析完报告结果后,用 requests 直接 post 一个 JSON。下面这段是完整的run_all.py骨架:

# run_all.py 执行入口 import unittest from utils.HTMLTestRunner import HTMLTestRunner from utils.mail_util import send_report from config import REPORT_PATH, BASE_URL def load_all_cases(): suite = unittest.defaultTestLoader.discover( start_dir="./testcases", pattern="test_*.py", top_level_dir=None, ) return suite def main(): suite = load_all_cases() with open(REPORT_PATH, "wb") as f: runner = HTMLTestRunner( stream=f, title="CRM 自动化回归报告", description=f"{BASE_URL} 全量回归", verbosity=2, ) runner.run(suite) send_report(REPORT_PATH, to_addrs=["tester@example.com", "dev@example.com"]) if __name__ == "__main__": main()

注意这里REPORT_PATH建议用绝对路径,因为 cron 启动时的当前目录可能和代码目录不同;to_addrs我建议从配置文件里读,不要硬编码到脚本里。另外一个实际开发中的细节:发件邮箱最好用公司自己的企业邮箱服务,用个人邮箱容易被服务器链当作垃圾邮件,特别是带有 HTML 附件的邮件,拦截率非常高。这一点等第五节再补充。

5. CRM 自动化实战的 5 个典型避坑点:从浏览器驱动到动态表格

5.1 浏览器驱动不匹配,启动就报错

现象:运行webdriver.Chrome()直接抛SessionNotCreatedException,提示「This version of ChromeDriver only supports Chrome version XX」。 原因:本地 Chrome 升级后又覆盖了驱动,或者用了版本差太多的驱动。 解决:换成 webdriver-manager,并在脚本里不要指定固定 driver 路径;如果公司内网拦截下载,就去别的机器缓存好驱动后用环境变量指定。另外,Linux 服务器上跑还要检查 Chrome 本身有没有装、有没有运行权限,常常是 Chrome 文件缺失导致报错。

5.2 表格数据多行,同一 class 元素取到一堆

现象:find_element明明能定位,但click()时操作的却不对,比如总是点击第一行的编辑按钮。 原因:CRM 表格每行按钮的 class 一样,find_element只会返回第一个匹配元素;你想操作第二行就定位不到。 解决:用行内文本来确定行索引,然后取该行范围内的子元素。具体 XPath 见 3.3 节的写法,这里再补一个常见的坑:如果你的表格有分页器,点击下一页后表格数据重新渲染,之前拿到的元素引用会过期,必须重新查找。我之前因为这个在报告里连续三天出现失败,后来养成了「每次操作前重新定位」的习惯。

5.3 unittest 用例执行顺序按字母序,破坏你的数据流程

现象:定义好的新增、查询、删除用例,跑出来顺序变成了test_01_delete、test_02_search之类的乱序。 原因:unittest 按方法名的 ASCII 码排序,test_后面第一位数字要按0、1、2顺序设计,但如果你写test_1_xxx和test_11_xxx,排序会变成test_1排在test_11之后,尤其两位数命名时容易乱。 解决:统一用test_01_、test_02_、test_10_、test_11_这种补零格式;test_10_永远排在test_02_之后是因为字典序「1」排在「0」后,小学问大麻烦。还有更好的思路是不要把用例写流依赖,用setUpClass准备数据。

5.4 HTMLTestRunner 在 Python3 下直接 import 报错

现象:import HTMLTestRunner时 SyntaxError 或 AttributeError。 原因:网上流传的 HTMLTestRunner 大多是 Python2 时代的版本,print语法和StringIO不兼容。 解决:不用 pip 的包,直接用修好的 py3 版 HTMLTestRunner 源码文件,或者改用 BeautifulReport。如果现在开始新项目,我建议直接选 BeautifulReport,省得踩这些翻车点。

5.5 邮件附件被当成垃圾邮件或中文名乱码

现象:报告发出去后被退信,或者收件人看到附件名是乱码。 原因:邮件服务器规则对 HTML 附件不信任;filename用 UTF-8 直接传导致消息头编码不对。 解决:一方面邮件主题和附件名尽量用英文,比如crm_test_report_2025.html;另一方面用filename=("utf-8", "", "中文名.html")正确编码。也可以把报告压缩成 zip 再发,误判率会明显下降。还有,确认发件邮箱的 SMTP host 和端口是否匹配,企业邮箱常用 465/587,别把两个端口换着试。

6. 让这套项目真正可维护:日志、重试与失败截图进阶技巧

自动化测试跑起来不难,难得是它不再天天变红。我最常用的一套维护组合是:失败自动截图 + 用例级重试 + 结构化日志。先看失败截图怎么和 unittest 挂钩。在tearDown里判断测试结果,如果是失败或错误,就把当前页面截图存到logs目录:

def tearDown(self): if not self._outcome.success: timestamp = time.strftime("%Y%m%d_%H%M%S") self.driver.save_screenshot(f"logs/failure_{self._testMethodName}_{timestamp}.png") self.driver.quit()

self._outcome.success是 unittest 内部属性,虽然带下划线但运行稳定。这样每次失败都自动留证据,邮件里不用附截图,直接去服务器翻 logs 就行。接着加重试。Selenium 里很多失败其实是一过性的,比如按钮点击时还在加载中,重试一次就好。我封装了一个简单的装饰器:

import time from functools import wraps def retry_on_failure(max_retries=3): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for i in range(max_retries): try: return func(*args, **kwargs) except Exception: if i == max_retries - 1: raise time.sleep(2) return wrapper return decorator

用的时候直接给测试方法或页面操作方法加一行@retry_on_failure(3),就能把偶发的网络抖动、弹窗遮罩等「假失败」滤掉。但注意:重试不适合断言失败,断言失败是真实业务问题,重试会掩盖 bug。我只对元素定位、点击这些浏览器交互操作重试。这套组合走完,你已经能应付大部分 CRM 自动化实战需求了。最后还有一个我经常对新人强调的习惯:脚本里所有等待数值、URL、用户名密码、邮箱配置全部抽到config.py,不要散在用例里。曾经为了改一个登录超时时间,我全局替换了 27 处,那种悔不当初的感觉希望你不用体会。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询