多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

用Jenkins打造自动化测试流水线:从安装配置到质量门禁实战

用Jenkins打造自动化测试流水线:从安装配置到质量门禁实战 我维护Jenkins流水线的时间算下来比写业务代码的时间还长。早些年大家一说CI/CD默认指的就是“部署流水线”——代码提交、构建、发布三步走看起来热闹实际上质量反馈特别滞后。真正让我对流水线改观的项目是某次重构后的核心服务开发分支一天几十次提交每次都要手动跑一轮接口回归测完再决定要不要上测试环境。那个月底我实在受不了了花了一周时间把整个流程塞进Jenkins从拉代码、跑单测、算覆盖率、部署测试环境到冒烟回归全部串成一条自动化测试流水线。从那个项目之后我自己的原则就变成流水线的价值不取决于发布按钮有多快而取决于测试环节卡得有多死。这篇文章就围绕“用Jenkins把自动化测试做成流水线里的一等公民”来写。我会从安装配置包括Docker部署、国内镜像加速、汉化这些容易卡壳的地方、声明式Pipeline核心语法与环境变量、Docker和Kubernetes动态构建节点、到测试阶段编排、质量门禁、自动部署联动最后附上我跑了几十条流水线之后攒下的排错经验。适合需要自己搭或长期维护Jenkins流水线的测试开发、DevOps工程师也包括想把手动回归流程变成可重复执行的团队的负责人。1. 先想清楚你搭的是“测试流水线”不是“发布按钮”很多团队一开始就把Jenkins当成“自动部署工具”装完立刻配一个拉代码、打包、丢服务器的任务跑通那一刻很有成就感。但没过多久就会发现部署没问题了测试还是原来的样子测试人员手动跑、夜里没人盯、用例多了等结果等到地老天荒。原因很简单你搭的是发布流水线不是测试流水线。1.1 测试流水线和部署流水线的本质区别部署流水线追求的是“快、稳、可回滚”核心指标是上线耗时和成功率。测试流水线追求的是“反馈质量、门禁可靠”核心指标是测试覆盖率、失败发现时间、误报率。两者目标不同阶段设计自然也不一样。部署流水线通常是构建 - 部署 - 冒烟干净利落。测试流水线则至少要有代码检出 - 依赖安装 - 静态分析 - 单元测试 - 测试报告汇总 - 部署到测试环境 - 接口或UI自动化回归 - 质量门禁。每个阶段都要考虑“失败了会产生什么后果”“哪个环节可以继续往下走”。我见过不少流水线把单测和部署混在一个stage里结果单测红了照样发布这等于质量门禁形同虚设。我的建议是新建流水线时就把阶段拆细。宁可流水线看起来长一点也不要为了视觉上的简洁把关键反馈点吞掉。1.2 什么项目最适合用Jenkins做测试流水线先说结论需要跑大量自动化测试、多分支同时开发、且要求数据不出内网的项目Jenkins依然是最能打的。这跟技术栈关系不大Java、Python、前端都可以用同一套Jenkins调度测试用例本身跑在哪、用哪个测试框架Jenkins只负责编排和收集结果。相对不适合的场景也很好判断如果项目只有一个仓库、一个主干分支、构建和测试加起来不超过十分钟那GitLab CI或者GitHub Actions的托管Runner就能解决没必要专门维护一台Jenkins。另外如果测试环境都是Serverless、按API调用计费的短时任务Jenkins这种常驻调度器反而显得笨重。1.3 动手前先把“门禁”定义清楚这是最容易被跳过的一步。我习惯在写Jenkinsfile之前先和测试负责人确认三件事哪些测试是必须通过才能继续的、覆盖率低于多少算不合格、测试报告里哪些数据要推送给团队群。门禁定义得越清楚后面Pipeline写起来越顺。否则就是流水线先搭好再反过来问测试“你们要不要卡一下”最后大概率变成“先别卡等稳定了再说”一等等半年。2. 从零装出一个能稳定跑半年的Jenkins这一部分讲安装和基本配置。很多教程直接给一段docker run就完事但我更想强调几个细节版本选择、挂载目录、镜像加速、汉化。这四个点处理好了后面半年能省大量运维时间。2.1 安装方式选型与Docker部署示例Jenkins常见的安装方式三种官方二进制包、Docker运行、Kubernetes Operator/Helm部署。个人建议中小团队直接用Docker单机测试环境最省事如果你们本来就有K8s集群且希望Jenkins高可用再用Helm上集群。选版本也有讲究。我身边踩过最多次的坑是装了个latest版本结果插件还没跟上界面变英文且一堆功能异常。以标题里提到的2.541.3为例它属于比较新的稳定线LTS版本通常插件兼容性好得多。我的原则能用LTS就不追新给插件生态留出适应时间。Docker部署命令先说一个最小可用的docker run -d \ --name jenkins \ -p 8080:8080 \ -p 50000:50000 \ -v /data/jenkins_home:/var/jenkins_home \ -v /var/run/docker.sock:/var/run/docker.sock \ -e JAVA_OPTS-Duser.timezoneAsia/Shanghai \ jenkins/jenkins:2.541.3注意几个关键点-v /data/jenkins_home挂载了JENKINS_HOME这是必须的不然容器一删所有任务、凭据、插件配置全丢挂载docker.sock是为了让Jenkins容器能直接操作宿主机Docker后面跑动态构建节点会用到50000端口是给动态Agent的JNLP连接用的云上部署时这个端口要确保可达很多动态节点连不上的问题都是因为这个端口被防火墙挡了。容器起来之后查看初始密码docker logs jenkins 21 | grep Initial admin password我在配置时还会顺手创建一个非root管理员账号把初始admin先禁用掉这台Jenkins如果暴露在公司内网弱权限管理迟早出问题。2.2 国内镜像拉取与加速配置安装Jenkins本身不难难的是镜像拉不动或者插件装不上。如果你在国内云环境直接docker pull jenkins/jenkins可能等到超时。这里可以用公共镜像加速地址比如阿里云的容器镜像服务个人加速器、中科大镜像站等在/etc/docker/daemon.json里配置{ registry-mirrors: [ https://docker.mirrors.ustc.edu.cn, https://你的阿里云加速地址.mirror.aliyuncs.com ] }改完记得sudo systemctl daemon-reload sudo systemctl restart docker docker info | grep -A 4 Registry Mirrors看到镜像源列表就说明生效了。注意这个配置只影响docker pull时的镜像搜索顺序不影响镜像仓库本身的网络情况。某个加速地址废了的话换一个再systemctl restart docker就行。插件装不动是另一个常见问题。默认Jenkins插件源在国外加载很慢。解决办法是在Jenkins系统管理 - 插件管理 - 高级里把升级站点的URL改成国内镜像地址。清华和华为云的Jenkins更新中心地址都可以用改完再刷新插件列表速度会明显改善。2.3 汉化与初始化配置汉化其实是三件套插件、语言设置、浏览器缓存。先装Locale Plugin和Localization: Chinese (Simplified)装完后在系统管理 - Locale里把默认语言设置为zh_CN并勾上Ignore browser preference and force this language to all users。不勾这个选项的话浏览器语言是英文的Jekins界面还是英文勾上之后记住清理浏览器缓存再刷新页面。初始化阶段还有两个容易被忽略的地方。第一是系统管理 - System里配置系统管理员邮箱测试流水线失败时邮件通知全指望这个邮箱第二是设置全局环境变量和工具路径比如JDK、Maven、Node的位置。用Docker镜像部署的话镜像内已经装了Jenkins自带的OpenJDK但Maven和Node通常要自己在“全局工具配置”里添加并指定自动安装版本。到这里一台能用的Jenkins就算装好了。3. 声明式Pipeline是流水线的骨架语法、环境变量和凭据Jenkins配好了接下来是核心工程写Jenkinsfile。强烈建议用声明式语法Declarative Pipeline它结构清晰、有语法校验团队其他人接手也容易看懂。3.1 一个最小可用的声明式Pipeline先给一个带测试阶段的最小示例别急着写复杂功能pipeline { agent any stages { stage(拉取代码) { steps { checkout scm } } stage(单元测试) { steps { sh python -m pytest tests/ --junitxmlreports/junit.xml } } stage(收集测试报告) { steps { junit reports/junit.xml } } } post { always { cleanWs() } failure { emailext subject: 构建失败: ${env.JOB_NAME} - ${env.BUILD_NUMBER}, body: 请查看 ${env.BUILD_URL} 里的失败原因。, to: teamexample.com } } }这个文件里agent指定流水线在哪个节点上跑any表示任意可用的执行器都行stages里是阶段列表每个stage可以有很多stepspost是阶段结束后固定执行的清理和通知逻辑。有一件事非常重要每一个阶段都应该有明确的失败反馈点。比如单测失败了流水线应该立刻停下来而不是继续往后部署。3.2 频繁使用的内置环境变量热词里提到的“jenkins可用环境变量”也是新手最容易懵的地方。小测试我整理一下声明式Pipeline里用得最多的是这些变量名含义使用场景BUILD_NUMBER当前构建编号构建号作为版本号后缀、通知消息里JOB_NAME任务名称通知、归档目录命名WORKSPACE工作目录绝对路径引用文件、清理缓存BUILD_URL这次构建的网页地址邮件/IM通知里给链接GIT_COMMIT当前构建对应的commit ID精确定位代码版本GIT_BRANCH拉取的分支分支场景下的部署/测试逻辑CHANGE_IDPR/MR的编号多分支流水线拉取请求触发的代码变更场景JENKINS_URLJenkins服务地址Agent侧回连、生成链接获取方式有两种env.BUILD_NUMBER或直接用$BUILD_NUMBER。实测中前者在Groovy脚本里更明确后者在shell命令里拼接字符串时更方便。有个细节容易踩坑GIT_BRANCH在部分插件版本里带origin/前缀在tag触发场景下值可能不一样写判断条件时最好先打印出来确认一次。3.3 凭据管理千万别把密码写进脚本自动化测试流水线免不了要访问Git仓库、服务器、K8s集群。切记不要把这些账号密码明文写进Jenkinsfile。Jenkins的凭据系统推荐用credentialsId引用。比如拉私有仓库时stage(拉取代码) { steps { checkout([$class: GitSCM, branches: [[name: env.GIT_BRANCH]], userRemoteConfigs: [[url: https://git.example.com/team/project.git, credentialsId: gitlab-account]]]) } }或者在shell里对敏感参数用withCredentials包裹stage(读取测试环境密钥) { steps { withCredentials([string(credentialsId: test-env-token, variable: ENV_TOKEN)]) { sh echo $ENV_TOKEN .env } } }凭据的credentialsId在系统管理 - Manage Credentials里创建创建后尽量只暴露credentialsId给Pipeline明文内容永远不出现在日志里。另外参数化构建里也不要把密钥作为参数传Jenkins日志会打出来的。3.4 参数化构建让测试任务可以“按需执行”自动化测试流水线不是千篇一律地跑全量用例经常需要按需选择。声明式Pipeline里可以用parameters块实现pipeline { agent any parameters { string(name: TARGET_ENV, defaultValue: staging, description: 测试环境) choice(name: TEST_LEVEL, choices: [smoke, regression, full], description: 测试级别) booleanParam(name: DEPLOY_AFTER_TEST, defaultValue: false, description: 测试通过后自动部署) } stages { stage(执行测试) { steps { sh pytest tests/ --level ${params.TEST_LEVEL} --env ${params.TARGET_ENV} } } } }触发方式也值得设计一下开发分支提交后可以自动跑smoke级测试深夜定时跑全量回归需要上线前手动触发一次带DEPLOY_AFTER_TESTtrue的流水线。这样既控制资源开销又把关键节点的反馈抓在手里。4. 用Docker和Kubernetes把构建节点“弹起来”测试流水线最怕的事情是几十个项目共用一个本地AgentWorkspace相互污染依赖冲突某次构建把Agent搞挂了后面全排队。解决思路很明确每次构建都跑在独立的容器环境里用完即弃。这就涉及Jenkins的动态节点了。4.1 Docker Cloud配置与“host uri root”问题Jenkins可以连接一台Docker主机作为“Cloud”按需启动容器作为构建节点。配置路径是Manage Jenkins - Manage Nodes and Clouds - Add a new cloud - Docker。很多人在填“Docker Host URI”这里卡住报错信息里会出现docker host uri root类似问题。这个报错通常是因为你写成了tcp://127.0.0.1:2375但Jenkins本身跑在容器里它的127.0.0.1指向的是Jenkins容器自己而不是宿主机。正确写法应该是宿主机IPtcp://192.168.1.10:2375或者如果你把/var/run/docker.sock挂载进了Jenkins容器可以直接用unix:///var/run/docker.sock连接宿主机Docker。选哪种取决于网络环境挂socket最稳但要注意权限用TCP方式要走TLS加密裸开2375端口放到外网等于给攻击者开门这个风险必须重视。配置Cloud时Remote File System Root建议写/home/jenkinsDocker Container template里填的镜像名要对应Agent镜像。很多团队的Agent镜像都是基于jenkins/agent再加工装好JDK、Maven、测试框架等。每个Agent容器启动后会通过JNLP协议连回Jenkins连不回来就检查50000端口和Agent镜像里的入口命令。4.2 容器内使用Docker命令的三种方案热词里“jenkins容器内使用docker命令”是个高频问题。本质上分三条路第一种最简单也是最危险的直接把宿主机的/var/run/docker.sock挂载进需要执行docker命令的容器。好处是docker命令直接操作宿主机Docker守护进程速度快缺点是这个容器一旦被攻破等同于控制整台宿主机。测试环境内部用没问题生产环境慎用。第二种是Docker-in-Docker再起一个特权容器做动态Agent在里面运行docker命令。隔离性好了但privileged: true本身引入的权限面也大而且镜像构建时的层缓存经常失效性能弱一些。第三种是尽量不直接调Docker守护进程镜像构建改用Kaniko、buildah这类不依赖socket的工具。安全性和可移植性最好代价是配置文件得调整、团队有个学习成本。我自己的选择是测试流水线里Agent容器和Jenkins容器都用挂socket的方案但Agent镜像里统一不允许挂宿主机任意目录只允许挂载Agent工作区。部署阶段要构建镜像时单独走Kaniko任务。下面这张表可以帮团队快速做决策方案隔离性安全性性能适用场景挂载docker.sock低低高内部测试环境、快速跑通Docker-in-Docker中中中需要特权模式、用例隔离Kaniko/buildah高高中生产环境镜像构建4.3 Jenkins 2.541.3配置Kubernetes动态节点如果你已经在用Kubernetes动态节点可以交给Kubernetes插件。在Manage Jenkins - Manage Nodes and Clouds里添加Kubernetes Cloud配置API地址、Namespace、Jenkins URL然后设置Pod Template。声明式Pipeline里可以直接用agentpipeline { agent { kubernetes { yaml apiVersion: v1 kind: Pod spec: containers: - name: jnlp image: jenkins/inbound-agent:latest args: [$(JENKINS_SECRET), $(JENKINS_NAME)] - name: maven image: maven:3.8-jdk-11 command: [cat] tty: true - name: pytest image: python:3.11-slim command: [cat] tty: true defaultContainer(jnlp) } } stages { stage(测试) { steps { container(pytest) { sh pip install -r requirements.txt pytest --junitxmlreport.xml } } } } }在这个例子里Pod里同时起了三个容器jnlp负责和Jenkins通信pytest容器才是真正跑测试的地方。用container(pytest)指定执行环境可以做到一个Pod内同时具备多种工具链相互不污染。配置时最常遇到的问题有两个一是Pod内的JNLP容器连不上Jenkins一要检查Jenkins URL是否从Pod内可达二是看Agent镜像有没有正确入口命令二是Pod动态创建失败后残留需要在Pod Template里设置retention策略比如podRetention选never任务结束就删。5. 测试阶段的全流程编排拉代码、跑测试、卡质量、再部署前面做了那么多准备真正核心的“自动化测试流水线”在这一节全面展开。我以一套常见的Web服务自动化测试流水线为例从代码拉取开始到质量门禁和部署联动。5.1 脚本拉代码与分支策略“jenkins脚本拉代码”这个热词背后的需求是很多人不想依赖SCM插件默认行为想自己控制拉取命令。我的习惯是能用checkout scm就用它顺便处理了凭据和多分支稳定可靠。如果确实需要自己写脚本可以参考这个模式if [ -d .git ]; then git fetch origin git checkout --force origin/${BRANCH_NAME} else git clone --branch ${BRANCH_NAME} https://git.example.com/team/project.git . fi注意--force是为了清理本地残留修改测试任务一般不需要保留工作区改动。BRANCH_NAME在多分支流水线里是插件自动注入的变量普通任务里可能要自己定义。还有一点测试流水线里拉代码往往要拉PR合并后的代码而不是PR来源分支这需要用CHANGE_TARGET这类变量做特殊处理不要想当然。5.2 单元测试、静态分析与覆盖率汇总跑测试的命令本身不复杂复杂的是把各类结果标准化。我习惯在Jenkinsfile里做如下编排stage(单元测试) { steps { sh pytest tests/unit --junitxmlreports/unit.xml \ --covsrc \ --cov-reportxml:reports/coverage.xml } post { always { junit testResults: reports/unit.xml, allowEmptyResults: false cobertura coberturaReportFile: reports/coverage.xml, failUnhealthy: true } } }这一段有两个关键设置。第一是allowEmptyResults: false它保证一条测试都没跑的时候流水线标记为失败。很多项目的测试流水线变成摆设就是因为XML路径配错但Jenkins没发现测试全没跑也显示绿。第二是覆盖率报告插件failUnhealthy可以设置覆盖率低于阈值时构建状态为不稳定。静态分析我一般独立成一个阶段服务端代码用SonarQube前端用ESLint和TS类型检查。SonarQube的Quality Gate可以作为硬门禁失败直接中断流水线。这阶段给团队带来的好处是“坏味道”和“致命缺陷”在一开始就暴露不会累积到上线前突然爆发。5.3 质量门禁怎么设计才能不流于形式很多人觉得门禁就是把JUnit结果挂上去、红了就失败。其实门禁至少要覆盖三个层面测试执行层面命令是否真的执行了、报告是否真的生成了用allowEmptyResults: false保证没有假绿。覆盖率层面新增代码的覆盖率是否达标老项目统一卡80%会导致全员抵触建议对新增代码单独卡阈值。缺陷密度层面SonarQube里是否引入了新缺陷、安全漏洞是否达到阻断级别。门禁的松紧要分阶段。刚上线的流水线我一般建议先“只报告不卡”跑一两周大家看到数据稳定了再逐步卡紧。一上来就卡死很容易触发团队逆反心理最后把流水线给废了。5.4 测试通过后联动自动部署热词里的“jenkins自动部署”正常是个水到渠成的环节。测试流水线里部署的不是生产环境而是测试环境部署策略可以激进一点自动化程度也可以高一点。stage(部署测试环境) { when { expression { params.DEPLOY_AFTER_TEST } } steps { withCredentials([string(credentialsId: kubeconfig, variable: KUBECONFIG)]) { sh echo $KUBECONFIG kubeconfig.yaml kubectl --kubeconfig kubeconfig.yaml apply -f deploy/ kubectl --kubeconfig kubeconfig.yaml rollout status deployment/my-service --timeout180s } } } stage(冒烟回归) { when { expression { params.DEPLOY_AFTER_TEST } } steps { sh pytest tests/smoke --env ${params.TARGET_ENV} --junitxmlreports/smoke.xml } post { always { junit reports/smoke.xml } } }部署后紧接着跑一组冒烟回归这是防止“部署上去但服务不可用”的最后一道防线。如果部署后冒烟失败流水线要能自动触发回滚脚本。我自己一般保留最近两个可用镜像tag或Helm Chart版本回滚命令写在部署脚本旁边别等故障发生再临时查。6. 流水线跑久之后我攒下的排错清单最后这部分把我的日常排错经验整理成清单。这里面的问题不会在刚搭好时出现但流水线跑上几个月大概率都会遇到。6.1 汉化、时区、中文乱码三件套前面讲了汉化插件的配置这里补充两个后续问题的排查方向。任务日志里中文全变??通常不是Jenkins的问题而是Agent容器里没有设置UTF-8环境变量在Agent镜像里加ENV LANGC.UTF-8 LC_ALLC.UTF-8即可。时区乱了则看JAVA_OPTS容器部署就用开头的-Duser.timezoneAsia/Shanghai系统级测试时间戳建议在environment块里统一设置TZ。6.2 动态节点断连和资源回收动态节点用得多了最常见的是两类问题。一类是经常断连JNLP Agent在Pod里运行默认连接超时时间比较短网络稍有波动就掉线解决办法是给Pod Template增加idleMinutes和连接重试设置或者把Jenkins URL改成内网稳定域名而非IP。另一类是资源泄漏Pod任务结束后一直不消失或Workspace文件越来越多检查Pod Template里的podRetention和容器镜像里的入口命令。我建议设podRetention: never同时给K8s集群里的Agent加上资源配额limit和闲置回收机制否则夜里跑的全量回归可能把集群节点内存打满。6.3 日志排查与Blue Ocean看板习惯流水线状态红了第一件事不是去翻代码而是先看日志链路构建页面的Console Output最直接然后到对应Agent上确认工作目录最后看Jenkins系统日志docker logs -f jenkins错误堆栈一般都指向明确。我强烈建议给团队装上Blue Ocean插件它把Pipeline阶段可视化成甘特图每个阶段耗时、失败点一目了然。测试趋势、历史失败的对比都比传统列表视图直观得多。排查的时候习惯性对比“上一次成功构建和这一次失败构建日志从哪一步开始不同”问题定位速度快得不是一星半点。6.4 最后分享两个长期维护的技巧第一升级插件前一定备份JENKINS_HOME最好直接做文件系统快照。插件升级导致配置丢失这种事我见得太多了。第二Jenkinsfile和测试代码放同一个仓库跟着分支走。流水线的改动也走MR评审不要直接在Jenkins网页上手工改。这样才能保证“流水线即代码”真正落地而不是某个人电脑里的私人配置。我自己的体会是自动化测试流水线的建设不是一个月搞定的大工程而是持续打磨的过程。先把基础跑通让数据说话再一步步把门禁收紧、把节点容器化、把部署联动起来。跑上一段时间你回头看最明显的变化不是“发布更快了”而是“回归测试不用人盯着了质量反馈稳定了”这才是Jenkins在这个场景下真正的价值。
返回列表