ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从能跑通到稳定落地:自动化测试项目实战复盘与CI/CD集成

从能跑通到稳定落地:自动化测试项目实战复盘与CI/CD集成 如果你的自动化脚本已经能跑通——登录、发请求、断言结果一百多条用例全绿——那恭喜你已经过了第一关。但我更想聊聊第二关这一百条用例在第二天早上还能不能全绿我自己做自动化测试项目时就死在这一关上过。框架写完了demo跑得很漂亮可真把项目接进来以后头一周就各种崩环境里的脏数据、接口返回顺序变化、页面某个按钮多等了几百毫秒、上一条用例留下的登录态把下一条用例带崩……这些事你在网上的教程里几乎看不到。因为所有教程都默认环境是干净的、数据是可控的、接口是稳定的。而真实项目的本质就是不可控。所以这一篇实战内容不打算再讲基础语法、pytest怎么装、断言怎么写——那些上一篇知识点总结里已经覆盖了。这篇要做的是把一个自动化测试项目从能跑的脚本推进到能稳定落地、能挂进CI、能给别人安全感的全过程复盘。如果你已经会写脚本、熟悉pytest但还没独立把一个完整项目做下来这篇文章应该正好是你缺的那块拼图。1. 被测系统摸底先搞清楚要打哪些点再决定怎么写脚本很多人拿到项目就急着写用例这其实是最容易翻车的起点。自动化测试本质上是在给系统建模你连系统的边界、依赖、数据流都没理清楚写出来的用例大概率是自嗨。我接手一个前后端分离的电商后台管理项目加配套App时先用了一天半做摸底不写一行代码。1.1 摸清系统架构与接口依赖前后端分离的项目前端是Vue或React后端是Java或Go中间走HTTP接口。摸底动作分三步找接口文档Swagger、YApi、Apifox都行没有文档就问开发要或者直接用抓包工具扒。重点不是看每个接口的参数而是梳理业务域——用户、订单、商品、支付、售后每个域之间怎么流转。确认接口的共性逻辑这个项目所有接口都要带token和签名参数那这俩东西就得做成公共处理而不是在每一条用例里各写一遍。搞清楚数据入口和出口数据从哪儿来App端、管理后台、定时任务写到哪个库中间经过哪些中间件。因为这个项目的下单接口还要扣减库存库存数据如果被其他线程改了接口可能报错——这种依赖在摸底阶段就应该暴露出来。1.2 圈定自动化范围哪些测、哪些不测这一步很多人偷懒觉得凡是能点的都测就是全面。实际不是。我习惯画一张核心业务流程网从用户注册、登录、浏览商品、下单、支付、查询订单、取消订单到后台的订单管理、商品上下架、对账统计把主链路先串出来。然后按投入产出比做取舍用例类型适不适合自动化理由核心业务主链路注册→下单→支付→查单强适合高频回归数据流长最容易出回归问题数据一致性校验接口返回和落库对照强适合机器比对最靠谱人工看不过来边界条件参数为空、超长、非法值适合一次写好反复执行成本极低纯视觉验证UI美观、布局错位不适合图片比对极不稳定维护成本高探索性测试随机点击、异常操作路径不适合人和AI的优势场景脚本做不好数据库无痕的只读用例适合不污染数据随便跑给一个实际比例参考接口层覆盖核心业务的70%以上UI层只覆盖关键用户路径和跨端交互App层再砍一半。记住一句话能用接口层覆盖的就别上UIUI脆弱、慢、维护贵。1.3 提前定下数据策略这条命脉这台项目里最常见的翻车就是数据测试库里有脏数据、用例之间互相污染、环境被定时任务重置。所以摸底阶段就要定三条规矩测试库独立绝不碰生产数据连备份恢复都不要在生产上做。每个用例自己造数、自己清理不依赖环境里刚好有某条数据。测试账号用代码自动创建接口注册、数据库直插都行别手工注册一百个账号再写死在脚本里。这三条能救命的规矩越早定越好。真到了用例跑挂再回头整理数据成本会翻好几倍。2. 把几十个散装脚本重构成能长期维护的测试框架脚本写多了你就会发现最痛苦的已经不是写用例而是维护。改一个接口地址几十个文件都要跟着改换一个人接项目完全看不懂你在写什么。这时候就需要框架化——不是为了让代码好看是为了让三个月后的你自己和你的同事还能改得动。2.1 分层目录结构所见即所管我最终落地的目录结构大概是这样的api_test_project/ ├── config/ │ ├── config.yaml # 环境配置地址、账号、数据库连接 │ └── env_config.py # 读取配置的入口 ├── data/ │ ├── test_cases/ # 数据驱动的用例数据JSON/YAML │ └── test_data_factory.py # 造数逻辑生成随机订单、随机用户 ├── common/ │ ├── base_api.py # 封装requests统一处理token、签名、日志 │ ├── base_page.py # UI层的页面基类封装元素等待、点击、输入 │ ├── assertion.py # 公共断言状态码、业务码、数据库校验 │ └── logger.py # 日志统一输出 ├── services/ │ ├── user_service.py # 注册、登录、改密 │ └── order_service.py # 下单、支付、查询、取消 ├── testcases/ │ ├── test_login.py │ ├── test_order_flow.py │ └── ui/ │ ├── test_login_page.py │ └── test_order_page.py ├── reports/ # 测试报告、截图、日志归档 └── conftest.py # pytest fixture集中管理2.2 为什么一定要有一层service封装很多初学者会把requests请求直接写在testcase里一开始确实跑得通。但项目一大就有两个问题一旦接口路径调整你要在几十个测试文件里找哪些用例调了这个接口一旦有业务参数变化你要在N个地方同步改。我在common/base_api.py里做好HTTP基础封装后又加了一层services把登录下单支付这些业务动作封装成领域函数。测试用例只关心业务步骤不关心HTTP细节。比如from services.order_service import OrderService def test_create_paid_order(user_session, config): order_id OrderService(user_session).create_paid_order( amount199, sku_idSKU_001 ) assert order_id is not None这样改一个接口URL只改service里一个函数所有用例全部免改。这层抽象的收益前两周感觉不出来跑到第三个月你就知道有多值了。2.3 fixture是pytest的灵魂别浪费conftest.py里我放了几个关键fixtureuser_session登录后的会话对象、fresh_order一条新创建的订单、admin_token后台管理员的token。fixture的好处是依赖注入和自动清理作用域控制好了用例之间天然隔离pytest.fixture(scopesession) def user_session(config): session UserService(config).login() yield session session.logout()注意我特意用了scopesession登录只做一次整套用例共享同一个会话。如果你不这样设计每条用例都登录一遍既慢又容易触发验证码拦截。3. 接口层实战登录态、依赖传递与返回200≠业务成功的断言接口自动化是整条链路里性价比最高的部分因为接口稳定、执行快、问题定位直接。但真实项目里你会遇到几个demo里不会有的麻烦。3.1 token和签名做成会话级fixture这个项目的接口有个特点登录拿token之后所有请求都要带token和签名签名是用密钥和时间戳算出来的。如果每条用例都去做签名那代码就炸了。正确做法是把签名逻辑封装进base_api的请求工具里token放到session级fixture里import hashlib import requests class BaseAPI: def __init__(self, host, tokenNone): self.host host self.session requests.Session() if token: self.session.headers[Authorization] fBearer {token} def post(self, path, **kwargs): url self.host path # 签名时间戳 密钥 体参数字典排序哈希 body kwargs.get(json, {}) sign self._make_sign(body) body[sign] sign resp self.session.post(url, jsonbody) self._log_request(path, resp) return resp def _make_sign(self, body): content .join(f{k}{v} for k, v in sorted(body.items())) return hashlib.sha256((content secret_key).encode()).hexdigest()为什么要放在会话对象里因为签名、token、公共header都属于每次请求都需要的上下文做成公共逻辑之后用例代码保持干净后面接CI时也方便统一管理。注意一个坑签名算法里如果有随机数或每次变化的字段一定要让双方统一规则否则线上环境明明好的测试环境全挂。3.2 接口依赖一个接口的返回值是另一个接口的入参最典型的就是下单→支付→查询。下单返回的订单ID是支付接口的入参支付成功后查询接口要能查到正确状态。这种链条如果用全局变量保存中间值跑到断言失败或用例乱序执行时会非常难受。我的做法是用fixture把链路上的产物逐级传递pytest.fixture def created_order(user_session): order_id user_session.create_order(amount88.5) return order_id pytest.fixture def paid_order(user_session, created_order): user_session.pay_order(created_order) return created_order def test_query_paid_order(user_session, paid_order): resp user_session.query_order(paid_order) assert resp[data][status] PAID这样用例和方法都变成依赖注入的写法每一段只管自己要做的事。fixture失败时pytest会自动跳过依赖它的用例这个特性在排查问题时特别好用——你会立刻知道是哪一步失败了。3.3 断言要分层200只是入场券返回200不代表业务成功这句话我念叨了很多遍因为它真的救过项目。当时有个下单接口状态码永远200但业务字段status在不同条件下会返回不同的错误码。手工测试可能注意不到但自动化用例如果只断言200等于没测。我的断言策略分三层第一层HTTP状态码判断服务通不通。第二层业务码和message判断业务成功与否code0或者successtrue这种。第三层关键业务数据落库校验。接口返回成功还不够我直接连测试库查一遍订单表的金额、状态、用户IDdef assert_order_in_db(conn, order_id, expected_status, expected_amount): cursor conn.cursor() cursor.execute( SELECT status, amount FROM orders WHERE order_id%s, (order_id,) ) row cursor.fetchone() assert row[0] expected_status assert float(row[1]) expected_amount为什么连数据库都要查有一次发版后接口返回的订单状态是对的但落库状态全乱了前端展示用的缓存还能看后台管理一查就露馅。如果没有数据库断言这问题根本暴露不出来。3.4 数据工厂让每条用例有自己干净的数据测试数据污染是所有接口层用例都会撞上的问题。比如注册用例第一次跑手机号18500001111成功。第二次跑手机号已存在失败。这不是用例写错了是数据没管控。解决方案是数据工厂每次生成唯一的数据import uuid class OrderFactory: staticmethod def build_order_payload(): return { order_no: fauto_{uuid.uuid4().hex[:12]}, amount: random.randint(1, 500), sku_id: SKU_001, }配合teardown里做清理要么调业务接口删单要么连数据库直接清理。这样每跑一次数据都是全新的一百次也不会撞车。这个习惯养成之后用例稳定性提升非常明显。4. UI层实战稳定性的敌人不是功能是定位、等待与脏数据UI自动化是整个自动化测试项目里最让人头疼的因为它的敌人太多前端改版、异步渲染、弹窗干扰、网络波动。但UI层有一个价值是接口层给不了的——它验证的是真实用户操作下界面状态正不正确。4.1 元素定位把优先级定死定位方式直接决定用例活多久。我踩过最深的一个坑就是前端把页面重构了一遍所有xpath全部失效我花了一个下午重写。后来再重构时我请前端同事在产品代码里埋了>from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def wait_and_click(driver, locator, timeout10): el WebDriverWait(driver, timeout).until( EC.element_to_be_clickable(locator) ) el.click()这里有个进阶技巧等元素消失比等元素出现更稳。比如提交订单后出现一个遮罩层遮罩层消失才表示提交完成。与其等提交成功文案它可能一闪而过不如直接等遮罩层消失。4.3 失败现场保留三件套截图、页面源码、控制台日志UI用例挂了最怕的是人没法复现。Selenium中用例失败会自动截图但真正排查问题光有截图远远不够。我的标准三件套是失败截图看到页面长什么样。页面HTML源码看到元素实际的状态、是否弹窗遮挡。浏览器console日志看到前端JS有没有报错。实现起来就是在conftest.py里加一个失败钩子pytest.hookimpl(hookwrapperTrue) 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(freports/{item.name}_failed.png) with open(freports/{item.name}_page.html, w, encodingutf-8) as f: f.write(driver.page_source) for line in driver.get_log(browser): logging.error(Browser console: %s, line)有一次排查一个按钮点了没反应的用例截图看不到任何异常一翻console才发现前端JS报了个未捕获的TypeError。没有日志这类问题要查到天亮。4.4 一次真实的全绿到全红事件这是我自己项目里印象最深的一次事故。周五下班前跑全量回归一百多条用例全绿。周一早上来跑挂了四十多条全是下单流程相关的。第一反应是昨天发版发坏了查了半天发现不是代码问题——是周六凌晨的定时任务把一张商品辅助表的数据清空了而下单流程强依赖那张表里的库存配置。这个事故告诉我一个道理环境是动态的不是你写完脚本它就静止等你。从那以后我做了一件事UI用例不再依赖环境里默认存在的数据每条用例在开始前主动构造所需数据结束后清理。虽然每次执行多了十几秒的准备工作但换来的是整个项目从三天两头修脚本变成稳定跑一个月不用动。这代价太值了。有一个数据是造不出来的——外部服务返回的固定响应。这种我后面会提到用mock方案解决UI层尽量别碰这类场景。4.5 Appium层的特例真机、弹窗和锁屏App端自动化比Web端多出来的麻烦主要是三块设备连接、系统弹窗、App状态。设备连接上用Appium跑真机时usb偶尔会断、手机锁屏导致会话失效。我的做法是专门搞一台Android测试机关掉自动锁屏开发模式下把所有自动更新系统弹窗通知全掐了。capabilities配置大致长这样desired_caps { platformName: Android, deviceName: emulator-5554, appPackage: com.example.app, appActivity: .MainActivity, noReset: True, newCommandTimeout: 120, }noReset尤其关键它决定App包数据和登录态清不清除。如果不清除登录态保留启动直接进首页如果每次都要重新走登录流程你会在弹窗和验证码上面耗尽耐心。系统弹窗用driver.context和driver.switch_to.alert去处理或者加一个启动后自动关闭已知弹窗的公共步骤。App启动参数、通知开关这些东西都放在capabilities里管理别散落在各条用例里。5. 把自动化挂进CI流水线让用例跑在固定的时间和环境里自动化测试如果不进CI价值直接砍半。因为只在本地跑的脚本等于靠自觉执行今天跑一下明天停一下根本形成不了回归屏障。5.1 三个执行时机三个不同用例集我不会让所有用例在每个触发点都跑一遍那样太慢也容易误报。我的设计一般是触发时机用例范围耗时目标目的代码Push后冒烟集核心主链路10~15条5分钟以内快速拦截低级错误每天凌晨全量回归接口UI30~60分钟全面体检早上上班看报告发版前全量回归数据一致性专项同上给发布信心具体到pytest里用marker来区分pytest.mark.smoke def test_login_and_query_product(user_session): ...执行时用pytest -m smoke只跑冒烟集pytest跑全量。5.2 Jenkins流水线不复杂但要稳定一个最小可用的Jenkins pipeline大概几十行就能搞定pipeline { agent any stages { stage(拉取代码) { steps { git branch: main, url: gitgitlab:test/autotester.git } } stage(环境准备) { steps { sh python3 -m venv venv sh source venv/bin/activate pip install -r requirements.txt } } stage(冒烟测试) { steps { sh source venv/bin/activate pytest -m smoke --tbshort } } stage(生成报告) { steps { sh source venv/bin/activate allure generate reports --clean } } } }注意一个细节CI节点上不要依赖任何本机状态。有人喜欢把浏览器driver、测试账号密码、配置文件放在Jenkins机器某个目录下换个节点就全挂。所有依赖都从代码仓库里拉配置从环境变量或config.yaml读取这才是可复现的自动化。5.3 UI自动化在Linux CI节点上的一个坑无头浏览器严格来说不算坑但确实要提前配好。我之前在Jenkins上跑Selenium一直报cannot find Chrome binary因为CI节点是个没有桌面环境的Linux服务器Chrome起不来。解决方案是给Chrome加headless参数options webdriver.ChromeOptions() options.add_argument(--headless) options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage)--disable-dev-shm-usage不加的话容器里可能会因为共享内存不足直接崩溃。这三个参数配合起来基本上就能在服务器上稳定跑UI用例了。5.4 失败通知要自解释自动化不是跑完就完了关键在于怎么让人看到结果。报告我用Allure生成HTML版塞进Jenkins但通知里不能只丢一个3条用例失败的链接。我习惯在钉钉/企微机器人的webhook推送里带上每个失败用例的摘要用例名、失败阶段、请求方法、响应状态码、错误断言、截图缩略图。这样收到消息的人不用登录Jenkins就能判断这是真问题还是一时的环境抖动。能先判断后处理人的响应效率高很多。失败用例的展示格式至少包含这三项信息用例名、期望vs实际、失败时的日志或截图路径。6. 复盘自动化项目烂尾的常见原因与我踩过的坑做自动化测试项目做到现在回头看看最开始的版本有一个很残酷的认知大部分自动化项目不是死在写代码上而是死在维护上。我把几个最常见的烂尾原因放在最后给后面再做类似项目的朋友当个参考。第一个原因是环境不稳定时硬上自动化。项目还在频繁改版、接口还没定、页面结构天天变这时候写的脚本就是一次性使用的。上自动化最好的时机是接口趋于稳定、UI明确不再大改的时候前期把精力放在维护手工回归清单和把接口文档规范好比急着写脚本更有用。第二个原因是追求框架大而全。我第一版框架塞了各种抽象封装、几十个公共方法最后真正用上的不到三分之一。代码行数是多了可维护性反而降了。框架不是越复杂越好而是刚好够用、别人能看懂最好。三层结构config、services、conftest已经能覆盖大部分场景别再往上面堆东西了。第三个原因是数据治理不够认真。我再强调一次自动化测试最大的敌人不是代码是脏数据。宁可多写造数代码也别依赖环境里刚好有的数据。那种今天能跑、明天挂了、后天看它心情的用例八成都是数据问题不是脚本问题。第四个原因是把自动化写成了打字机。如果只是把手工用例原封不动翻译成脚本自动化就是在给手工测试员当打字员。手工测试强调探索自动化适合重复回归。用例的设计顺序应该是核心业务主链路 高频回归 数据一致性 边界条件别把顺序倒过来先在边角料上浪费大量力气。关于UI自动化的投入我个人的经验是保持克制。接口自动化一天能覆盖的范围比UI层一周写的还大而且稳定得多。UI层就守住用户最关键的几条主链路剩下的交给接口层去覆盖。钱要花在刀刃上自动化测试的投入也一样。最后再分享一个从全红到全绿的突破口。当时我接手一个快被放弃的自动化项目所有用例都是先查环境里有没有数据有就用没有就手工造一条。结果就是用例互相依赖、状态互相干扰。后来我痛下决心把每条用例改成自己造数、自己清数、不依赖任何共享数据两个星期后项目从天天修脚本变成可以一整周不用管。所以如果你现在也困在这个泥潭里别去优化什么等待策略、定位方式先动数据治理。自动化测试这份工作做久了你会发现技术从来不是最大的瓶颈。真正的瓶颈是你能不能把脚本能跑升级成永远稳定地跑而这一步的答案往往藏在用例设计、数据治理和环境预期这些不起眼的细节里。
返回列表