前端自动化的锅,我背了三年

高庆丰
2025-12-26 12:24
阅读 1118

成都的夏天闷得像泡在火锅底料里,上周五晚上十点半,我还在公司对着 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

核心逻辑其实就三步:

  1. 调 Spring Boot 的 /api/test/prepare 准备环境
  2. 启动对应的 Puppeteer 脚本
  3. 把结果 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

最热最新
暂无评论
高庆丰Lv.1
0
影响力
0
文章
0
粉丝