简介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 ModelPOM来组织目录大概长这样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[typesubmit]).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(namename, contactcontact, phonephone, industryindustry) 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, patterntest_*.py) with open(./reports/crm_test_report.html, wb) as f: runner HTMLTestRunner( streamf, titleCRM 自动化回归报告, description用例执行统计, verbosity2, ) runner.run(suite)defaultTestLoader.discover会自动查找testcases目录下所有test_*.py文件里的测试类拼装成 suite。HTMLTestRunner的stream是指输出文件对象必须用二进制模式写title和description会显示在报告头部verbosity2表示每个用例的详细执行结果都会输出到报告里。生成之后直接打开 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)是为了避免中文文件名乱码如果文件名是英文写成filenamecrm_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, patterntest_*.py, top_level_dirNone, ) return suite def main(): suite load_all_cases() with open(REPORT_PATH, wb) as f: runner HTMLTestRunner( streamf, titleCRM 自动化回归报告, descriptionf{BASE_URL} 全量回归, verbosity2, ) runner.run(suite) send_report(REPORT_PATH, to_addrs[testerexample.com, devexample.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(flogs/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_retries3): 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 处那种悔不当初的感觉希望你不用体会。希望帮到你。本文还有配套的精品资源点击获取