前端自动化的锅,我背了三年
成都的夏天闷得像泡在火锅底料里,上周五晚上十点半,我还在公司对着 VSCode 里一堆红叉叉发呆。产品经理刚甩过来一句“能不能让测试同学少点几下鼠标?”,我就知道,又一个自动化脚本的坑要填了。
作为团队里那个“坚持手写每一行代码”的老古董(其实只是对 Copilot 的建议经常翻白眼),我向来对所谓的“智能辅助”持保留态度。但现实很骨感——我们这个小团队,三个前端、两个后端、一个全栈(其实就是我),每天被各种重复性任务压得喘不过气:部署前跑 lint、构建后检查包体积、上线前回归核心流程……不自动化?那真得 996 到天荒地老。
所以今天这篇,就聊聊我怎么用 JavaScript + Spring Boot 搞定一套轻量级自动化脚本体系,把那些烦人的机械操作从日常工作中踢出去。虽然过程有点曲折,但最终效果还不错——至少上周五不用加班到凌晨了。
需求不是来自技术,而是来自吐槽
事情得从去年双11说起。当时我们搞了个大促活动页,交互复杂得堪比小游戏——粒子动画、手势滑动、实时倒计时、动态商品流,全堆在同一个页面上。上线前夜,测试同学崩溃了:“这页面我得手动点三十多个路径才能覆盖完主流程!”
更惨的是,每次前端改个动画参数,后端调个接口字段,就得重新走一遍。有一次因为一个 setTimeout 写成了 setInterval,导致用户停留超过 5 分钟页面直接卡死,线上报错刷屏运维群。那天我差点把 MacBook 掏出来砸了——不是因为 bug 本身,而是因为我们居然没有自动回归的能力。
领导拍板:“搞个自动化方案吧。” 于是,这个“锅”自然落到了我头上——谁让我平时总吹“代码要可维护、可测试、可自动化”呢?
技术选型:能用 JS 就别碰 Python(开玩笑的)
一开始我想直接上 Cypress 或 Playwright,毕竟前端出身,对这些工具熟。但问题来了:我们的后端是 Spring Boot,很多接口需要特定的认证上下文(比如企业微信登录态),而且部分业务逻辑涉及数据库状态重置(比如清空用户的优惠券记录)。
如果只用前端 E2E 工具,就得额外写 mock server 或者搞复杂的 proxy 配置。与其绕弯子,不如让前后端各干各的活:前端负责 UI 自动化,后端负责数据准备和 API 脚本调度。
于是架构就这么定了:
- 前端层:用 Puppeteer 写浏览器自动化脚本,模拟真实用户操作
- 后端层:用 Spring Boot 提供 RESTful 接口,用于初始化测试数据、触发脚本、获取结果
- 调度层:用 Node.js 写一个轻量 CLI,串联前后端能力,支持本地运行 & CI 集成
为什么选 Puppeteer 而不是 Playwright?坦白说,主要是我本地 VSCode 里已经装了一堆 Puppeteer 相关插件(比如 Puppeteer Recorder),而且团队里没人会写 Python。虽然 Playwright 更现代,但“能跑就行”才是小团队的生存哲学。
第一版脚本:跑通了,但丑得想哭
先看个最简单的场景:用户登录后进入首页,检查是否加载了商品列表。
前端脚本大概是这样:
// scripts/e2e/homepage.test.js
const puppeteer = require('puppeteer');
async function testHomepage() {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
try {
// 1. 访问登录页(带测试 token)
await page.goto('http://localhost:3000/login?testToken=mock_123');
// 2. 等待跳转到首页
await page.waitForNavigation();
// 3. 检查商品容器是否存在
const hasProductList = await page.$('.product-grid') !== null;
if (!hasProductList) {
throw new Error('商品列表未加载');
}
console.log('✅ 首页加载成功');
return { success: true };
} catch (err) {
console.error('❌ 首页测试失败:', err.message);
return { success: false, error: err.message };
} finally {
await browser.close();
}
}
module.exports = { testHomepage };
看起来还行?但问题很快暴露了:
- 每次都要手动改
testToken - 如果后端服务没启动,脚本直接崩
- 没有统一的结果上报机制,CI 里看不到详细日志
这时候 Spring Boot 后端就派上用场了。我们在后端加了个 /api/test/prepare 接口,专门用来生成临时测试凭证并重置数据库状态:
// TestController.java
@RestController
@RequestMapping("/api/test")
public class TestController {
@Autowired
private UserService userService;
@PostMapping("/prepare")
public ResponseEntity<TestContext> prepareTestContext(@RequestBody TestRequest req) {
// 1. 创建测试用户
User testUser = userService.createTestUser(req.getScenario());
// 2. 生成短期有效的 JWT
String token = JwtUtil.generateTestToken(testUser.getId(), Duration.ofMinutes(10));
// 3. 返回上下文给前端脚本
return ResponseEntity.ok(new TestContext(token, testUser.getId()));
}
}
这样一来,前端脚本就能先调后端拿 token,再启动浏览器:
// 改进后的脚本
const axios = require('axios');
async function testHomepage() {
// 先请求后端准备环境
const { data } = await axios.post('http://localhost:8080/api/test/prepare', {
scenario: 'homepage'
});
const browser = await puppeteer.launch();
const page = await browser.newPage();
try {
await page.goto(`http://localhost:3000/login?token=${data.token}`);
// ...后续逻辑不变
} finally {
await browser.close();
}
}
终于,脚本不再依赖硬编码的 token 了!但新问题又来了:本地开发时端口乱七八糟,CI 环境又是另一套配置。
配置管理:别让环境变量毁了你的周末
我曾经天真地以为 .env 文件能解决一切。直到某次把本地数据库密码提交到 Git,被运维大哥在群里@了三次。
后来我们搞了个分层配置策略:
| 环境 | 配置来源 | 示例值 |
|---|---|---|
| 本地开发 | config.local.json |
http://localhost:3000 |
| CI 测试 | 环境变量(CI 系统注入) | http://ci-preview.example.com |
| 生产演练 | Spring Boot Config Server | 加密的远程配置 |
Node 脚本启动时自动合并配置:
// utils/config.js
const fs = require('fs');
const path = require('path');
function loadConfig() {
let config = {};
// 优先读取环境变量
if (process.env.TEST_FRONTEND_URL) {
config.frontendUrl = process.env.TEST_FRONTEND_URL;
}
// 本地开发 fallback 到文件
if (!config.frontendUrl && fs.existsSync('config.local.json')) {
config = { ...config, ...require('./config.local.json') };
}
return config;
}
module.exports = { loadConfig };
Spring Boot 那边也做了 profile 隔离:
# application-ci.yml
test:
token-expire-minutes: 5
db-reset-enabled: true
# application-prod.yml
test:
token-expire-minutes: 1
db-reset-enabled: false # 生产禁止重置数据!
这招救了我好几次——特别是当测试同学在非 CI 环境误触发脚本时,至少不会清掉生产数据库(虽然我们根本没有生产权限,但吓出一身冷汗是真的)。
调度器:让脚本自己跑起来
光有脚本还不够,得有人“喊开始”。我们写了个简单的 CLI 工具 auto-runner,用 Node.js 实现:
# 安装
npm install -g @our-team/auto-runner
# 运行单个测试
auto-runner run homepage
# 运行整个套件
auto-runner suite smoke
# 查看报告
auto-runner report --last
核心逻辑其实就三步:
- 调 Spring Boot 的
/api/test/prepare准备环境 - 启动对应的 Puppeteer 脚本
- 把结果 POST 回后端
/api/test/report
后端用一张表存历史记录:
CREATE TABLE test_runs (
id BIGINT PRIMARY KEY,
suite_name VARCHAR(50),
case_name VARCHAR(100),
status ENUM('PASS', 'FAIL'),
duration_ms INT,
error_message TEXT,
created_at TIMESTAMP
);
这样,前端同学在 VSCode 里按个快捷键(我绑了个 task),就能看到最近一次测试结果;产品经理也能去内部系统看“自动化通过率”——虽然他可能根本看不懂,但至少不再半夜微信问我“测了吗?”。
坑与教训:有些雷踩一次就够了
当然,过程没那么顺利。分享几个血泪教训:
1. 别信“headless: true”万能
Puppeteer 默认 headless 模式在 CI 里跑得飞快,但本地调试时根本看不出页面到底卡在哪。后来我加了个 --debug 参数,自动打开 devtools:
const isDebug = process.argv.includes('--debug');
await puppeteer.launch({
headless: !isDebug,
devtools: isDebug
});
现在 debug 起来爽多了,虽然每次开 devtools 都感觉电脑风扇要起飞。
2. 异步等待别偷懒
早期我常用 page.waitFor(2000) 这种 sleep 大法,结果网络一慢就挂。后来全部改成显式等待:
// 错误示范
await page.waitFor(2000);
// 正确姿势
await page.waitForSelector('.loading', { hidden: true });
await page.waitForFunction(() => window.appReady === true);
3. 日志要可追溯
最初脚本报错只打印 “Error: Navigation timeout”,根本不知道是哪个步骤。现在每个关键操作都打 tag:
console.log('[STEP] 等待登录完成...');
await page.waitForNavigation();
console.log('[STEP] 检查商品列表...');
配合后端的日志链路 ID,排查速度快了一倍。
效果:从天天加班到准时下班
这套东西搞完后,最直观的变化是——测试回归时间从 2 小时缩短到 8 分钟。以前每次发版前,测试同学得手动点几十个路径;现在 CI 里自动跑,失败了才人工介入。
更重要的是,我们敢重构了。上周我把首页的动画库从 GreenSock 换成 Framer Motion,跑一遍自动化脚本,确认所有交互动效正常,就直接合了。搁以前,光回归就得半天。
当然,它不是银弹。复杂的业务逻辑(比如支付流程)还是得人工验证,但至少把那些“点按钮 -> 看结果”的体力活自动化了。
最后一点碎碎念
有人说,前端搞自动化是不是越界了?我觉得,在小团队里,边界本来就是模糊的。既然痛点在我这儿,那就想办法解决,管它用 JS 还是 Java。
而且说实话,写这些脚本的过程,反而让我更理解后端同学的苦——原来他们每次改个 DTO 字段,前端就要跟着调,确实挺烦的。现在我们约好了:接口变动必须同步更新自动化脚本,否则 PR 不给过。
回到开头那个周五晚上。当我看到终端输出 All tests passed ✅ 时,窗外的成都已经灯火通明。收拾电脑,走出公司,火锅店还在营业——这才是生活该有的节奏。
至于 AI 辅助?嗯,Copilot 刚帮我补全了一个 waitForFunction 的回调,勉强算帮了忙吧。但核心逻辑,还是得自己写,心里才踏实。

评论 0