Git + Jenkins 自动化测试流水线:逐阶段避坑指南¶
这篇不是流程概述,而是一条跑过真实项目的流水线:每个 stage 为什么这么写、失败时的原始报错是什么、根因在哪、怎么改。
0. 先看整体结构¶
flowchart LR
A[git push] --> B[Checkout]
B --> C[依赖安装]
C --> D[静态检查]
D --> E[接口测试]
E --> F[UI 冒烟]
F --> G[Allure 报告]
G --> H[通知/归档]
约束先定死,后面每一步都围绕它:
| 约束 | 取值 | 原因 |
|---|---|---|
| 单次流水线时长 | < 25 分钟 | 超过后没人等结果,PR 会先被合并 |
| 每个 stage 超时 | 10 分钟 | 防止 UI 用例挂死拖满 executor |
| 用例并行度 | 4 | 8C16G runner 的经验上限,再高浏览器抢 CPU |
| 报告保留 | 30 次构建 | 足够定位“上周还好好的”问题 |
| 失败策略 | smoke 失败即停,regression 允许部分失败 | 保证反馈速度 |
1. 执行机镜像:所有“诡异失败”的根源¶
UI 用例在 CI 上挂,八成不是脚本问题,而是镜像缺东西。用固定版本的 Dockerfile,别用 latest。
FROM python:3.11-slim-bookworm
ENV DEBIAN_FRONTEND=noninteractive \
TZ=Asia/Shanghai \
LANG=C.UTF-8 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=0
RUN apt-get update && apt-get install -y --no-install-recommends \
chromium chromium-driver \
fonts-noto-cjk fonts-noto-color-emoji \
tzdata ca-certificates curl jq \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /workspace
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
三个细节值得单独说:
fonts-noto-cjk:缺中文字体时页面文字宽度变化,元素位置偏移,点击会落到别的元素上;截图里中文全是方块。fonts-noto-color-emoji:带 emoji 的按钮文案定位不到(get_by_text("提交 ✅")匹配失败)。- 固定
chromium与chromium-driver同源安装:避免 driver 与浏览器版本不匹配的session not created: This version of ChromeDriver only supports Chrome version 114。
2. Checkout:浅克隆的两个坑¶
stage('Checkout') {
steps {
checkout([
$class: 'GitSCM',
branches: [[name: '*/main']],
userRemoteConfigs: [[
url: 'git@gitlab.example.com:qa/api-tests.git',
credentialsId: 'gitlab-deploy-key'
]],
extensions: [
[$class: 'CloneOption', depth: 1, shallow: true, timeout: 5],
[$class: 'SubmoduleOption', disableSubmodules: false, recursiveSubmodules: true]
]
])
}
}
坑 1:depth: 1 + 需要 git describe 取版本号时拿不到 tag,报 fatal: No names found, cannot describe anything。解法是加 [$class: 'GitTag'...] 或改用 git rev-parse --short HEAD。
坑 2:测试数据放在 submodule 里,浅克隆默认不拉,用例报 FileNotFoundError: data/users.csv。必须显式开 recursiveSubmodules。
3. 依赖安装:缓存与“锁文件漂移”¶
stage('Install') {
steps {
sh '''
set -euo pipefail
python -m pip install --upgrade pip
# 依赖不变时命中缓存,避免每次重装 3 分钟
pip install -r requirements.txt
'''
}
}
真实问题与解法:
| 报错 | 根因 | 解法 |
|---|---|---|
error: externally-managed-environment |
Debian 12 的 PEP 668 保护 | 用虚拟环境 python -m venv .venv 或在镜像里装 |
本地过、CI 挂,报 ImportError |
requirements.txt 未锁小版本 |
pip freeze > requirements.lock,CI 装 lock |
| 每次构建耗时 4 分钟 | 没复用 pip cache | Jenkins 里 --cache-dir 挂持久目录,或做一层基础镜像 |
4. 静态检查:让它“阻断”而不是“提示”¶
stage('Lint') {
steps {
sh '''
ruff check . --output-format=github
mypy apis core --ignore-missing-imports || true
'''
}
}
建议只让 ruff(或 flake8)阻断,mypy 先观察。原因:类型检查在历史代码上一次性全绿的成本很高,强行阻断会让人直接关掉这个 stage。
5. 接口测试:环境、密钥与“假绿”¶
stage('API Test') {
environment {
BASE_URL = 'https://test.example.com'
}
steps {
withCredentials([
string(credentialsId: 'qa-api-user', variable: 'API_USER'),
string(credentialsId: 'qa-api-password', variable: 'API_PASSWORD')
]) {
sh '''
set -euo pipefail
pytest -m "smoke or regression" \
--alluredir=allure-results \
--junitxml=reports/junit.xml \
-p no:cacheprovider
'''
}
}
post {
always {
junit 'reports/junit.xml'
archiveArtifacts artifacts: 'allure-results/**', allowEmptyArchive: true
}
}
}
这里的四个“假绿”必须堵住:
- 只断言 HTTP 200:业务失败也返回 200。必须断言业务 code。
--alluredir写相对路径:stage 切换目录后报告找不到,表现为 Allure 页面空白或There are no results to show。统一写绝对路径${WORKSPACE}/allure-results。pytest返回码被管道吞掉:pytest ... | tee log的退出码是tee的 0。要么set -o pipefail,要么用pytest ... 2>&1 | tee log; exit ${PIPESTATUS[0]}。- 用例依赖上次数据:CI 是干净环境,必须自带造数与清理,不能依赖“库里有这条订单”。
6. UI 冒烟:容器内 Chrome 的五个必配参数¶
# core/driver.py
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
def build_driver() -> webdriver.Chrome:
o = Options()
o.add_argument("--headless=new")
o.add_argument("--no-sandbox") # 容器内无 user namespace
o.add_argument("--disable-dev-shm-usage") # /dev/shm 只有 64MB
o.add_argument("--disable-gpu")
o.add_argument("--window-size=1440,900") # 无头模式默认 800x600,响应式布局会变
o.add_argument("--lang=zh-CN")
o.add_experimental_option("prefs", {
"intl.accept_languages": "zh-CN,zh",
"credentials_enable_service": False,
})
return webdriver.Chrome(options=o)
对应报错与根因:
| 现象 | 原始报错 | 根因 | 解法 |
|---|---|---|---|
| 浏览器起不来 | Chrome failed to start: crashed / DevToolsActivePort file doesn't exist |
容器无 sandbox 权限 | --no-sandbox |
| 打开大页面即崩 | unknown error: session deleted because of page crash |
/dev/shm 默认 64MB |
--disable-dev-shm-usage 或 --shm-size=2g |
| 点击被拦 | ElementClickInterceptedException: element click intercepted: Element <button> is not clickable at point (x, y). Other element would receive the click |
固定遮罩/动画/吸底栏 | 等遮罩消失,或 scrollIntoView + JS 点击 |
| 元素引用失效 | StaleElementReferenceException: element is not attached to the page document |
列表重渲染 | 重新定位,用返回元素的方法而非缓存 WebElement |
| 找不到元素 | NoSuchElementException |
在 iframe / Shadow DOM / 新标签页 | 切 context 后再定位 |
如果 UI 用例跑在 compose 里,别忘了:
services:
chrome:
image: selenium/standalone-chromium:4.25.0
shm_size: 2gb
ipc: host
environment:
- SE_SCREEN_WIDTH=1440
- SE_SCREEN_HEIGHT=900
- TZ=Asia/Shanghai
7. 并行:快,但会踩数据¶
stage('Regression') {
failFast true
parallel {
stage('API') {
steps { sh 'pytest -m api -n 4 --dist loadscope --alluredir=allure-results-api' }
}
stage('UI') {
steps { sh 'pytest -m ui -n 2 --alluredir=allure-results-ui' }
}
}
}
-n 4并行后,两个 worker 同时抢同一个测试账号,必现“库存不足”“订单已被支付”。- 解法:worker 维度隔离数据,
pytest-xdist提供worker_id:
@pytest.fixture(scope="session")
def api_user(worker_id):
suffix = "master" if worker_id == "master" else worker_id
return create_user(f"qa_ci_{suffix}") # 每个 worker 独立账号
- UI 并行还有浏览器实例数上限:2 个 Chrome 在 2C4G runner 上就会互相拖慢,P95 翻倍。并行度要按 runner 规格定,不是越多越快。
8. Allure 报告:路径、历史与“空报告”¶
post {
always {
allure includeProperties: false,
jdk: '',
results: [[path: 'allure-results*']],
reportBuildPolicy: 'ALWAYS'
}
}
三个高频坑:
results只写了一个目录,而并行 stage 写了allure-results-api、allure-results-ui,报告显示不全。用 globallure-results*或统一写入同一目录。- Allure 插件未安装:pipeline 直接报
No such DSL method 'allure' found。装Allure Jenkins Plugin并在全局工具里配置allure命令行(或让插件自动安装)。 - 历史趋势断档:
allure-results被cleanWs()删掉后,趋势图每次都从零开始。报告生成前不要清理 workspace,或使用allure的 history 目录挂载。
9. 通知:只发结论,不发日志¶
post {
failure {
sh '''
curl -s -X POST "$WEBHOOK" -H 'Content-Type: application/json' \
-d "{\\"msgtype\\":\\"markdown\\",\\"markdown\\":{\\"content\\":\\"**构建失败** [$JOB_NAME #$BUILD_NUMBER]($BUILD_URL)\\n阶段: 见控制台\\"}}"
'''
}
}
通知里放三样就够:谁失败了、哪个阶段、报告链接。把 500 行日志贴进群,只会让人屏蔽机器人。
10. 完整 Jenkinsfile¶
pipeline {
agent { label 'qa-runner' }
options {
timeout(time: 30, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '30', artifactNumToKeepStr: '10'))
disableConcurrentBuilds(abortPrevious: true)
ansiColor('xterm')
}
environment {
TZ = 'Asia/Shanghai'
ALLURE_DIR = "${WORKSPACE}/allure-results"
BASE_URL = 'https://test.example.com'
}
stages {
stage('Checkout') {
steps { checkout scm }
}
stage('Install') {
steps {
sh 'python -m pip install -r requirements.txt'
}
}
stage('Lint') {
steps { sh 'ruff check . --output-format=github' }
}
stage('API Smoke') {
steps {
withCredentials([string(credentialsId: 'qa-api-token', variable: 'API_TOKEN')]) {
sh '''
set -euo pipefail
pytest -m smoke \
--alluredir="$ALLURE_DIR" \
--junitxml=reports/junit.xml
'''
}
}
post {
always {
junit allowEmptyResults: true, testResults: 'reports/junit.xml'
}
}
}
stage('Regression') {
when { branch 'main' }
failFast true
parallel {
stage('API') {
steps {
sh 'pytest -m regression -n 4 --alluredir="$ALLURE_DIR"'
}
}
stage('UI') {
steps {
sh 'pytest -m ui_smoke -n 2 --alluredir="$ALLURE_DIR"'
}
}
}
}
}
post {
always {
allure includeProperties: false,
jdk: '',
results: [[path: 'allure-results']],
reportBuildPolicy: 'ALWAYS'
archiveArtifacts artifacts: 'reports/**,logs/**,screenshots/**', allowEmptyArchive: true
}
cleanup {
// 只清构建产物,保留 allure-results 供报告归档
sh 'find . -name "__pycache__" -type d -prune -exec rm -rf {} + || true'
}
}
}
11. 失败排查顺序¶
出现红构建,按这个顺序看,比从头翻日志快:
1. Console 最后的 "ERROR:" 行 → 判断是环境、依赖还是用例
2. Allure 报告 → 失败用例的 step 与 attach 的请求/响应
3. reports/junit.xml → 失败数量与耗时分布
4. 截图与 page source → UI 失败时先看页面停在哪个状态
5. 用例日志中的 trace id → 去服务端日志查同一次请求
12. 验收标准¶
流水线改完后,用下面几项确认它真的可用:
- 空 commit 触发一次,耗时应 < 预算的 80%
- 故意让一条 smoke 断言失败,构建必须红,且 Allure 能定位到具体 step
- 删除测试环境的一条基础数据,回归应报错而不是“假绿”
- 并行跑 3 次,结果集合应一致(无偶发失败)
- Allure 趋势图连续 5 次构建有数据