若依系统自动化与性能测试实战:从框架选型到CI/CD集成
1. 项目概述当若依系统遇上自动化与性能测试最近在带团队做项目复盘发现一个挺有意思的现象很多团队在用若依RuoYi这类成熟的后台管理系统框架时开发效率确实上去了但一到测试环节就容易“抓瞎”。特别是当项目进入中后期功能模块越堆越多每次回归测试都像是一场人力与时间的消耗战。更头疼的是系统上线后用户量一上来页面加载变慢、接口超时的问题就开始冒头。所以这次我们拿一个典型的“若依系统”开刀目标很明确——给它套上“自动化测试”的铠甲再装上“性能测试”的雷达看看如何系统性地提升这类项目的质量与稳定性。这不仅仅是完成一次作业更是解决一个非常实际的工程问题如何让一个基于流行框架的快速开发项目同样具备工业级的可靠性与可维护性。若依框架大家都不陌生它提供了一套开箱即用的后台管理解决方案权限、菜单、字典、监控等功能一应俱全。但正因为其“完整”我们往往会把测试重点放在自己写的业务代码上而忽略了框架本身集成带来的复杂性以及业务代码与框架交互可能产生的性能瓶颈。自动化测试就是为了把我们从重复的、枯燥的点击和表单填写中解放出来确保每一次代码提交都不会破坏原有功能。而性能测试则是要提前预知系统的承载能力找到那些在开发环境“岁月静好”一到生产环境就“原形毕露”的慢查询、内存泄漏或配置不当。这两者结合正是为若依这类系统从“能用”到“好用且稳定”保驾护航的关键。2. 自动化测试策略设计与框架选型给若依系统做自动化测试不能胡子眉毛一把抓。我们需要分层、分类型地构建测试体系。核心思路是“金字塔模型”底层是量大且运行快的单元测试和接口测试上层是量少但更贴近用户操作的UI测试。对于若依这种前后端分离的架构以RuoYi-Vue为例我们的测试重心应该放在哪里答案是接口自动化测试是核心UI自动化测试作为关键业务流程的补充单元测试则嵌入开发流程。2.1 接口自动化测试Playwright 还是 Requests若依的后端通常是Spring Boot提供RESTful API。做接口测试很多人第一反应是用Python的requests库或者Java的RestAssured。这没错但对于一个已经成型、且需要持续集成的项目我们更需要一个能兼顾编写效率、可维护性和报告清晰度的方案。我强烈推荐使用Playwright配合Python/TypeScript/Java来做接口测试。你可能会疑惑Playwright不是做浏览器自动化的吗没错但它提供的APIRequestContext功能让我们能像调用HTTP客户端一样轻松地发送请求、处理响应并且能天然地复用浏览器自动化中的认证状态如处理登录后的Cookie、Token。这对于测试若依这种需要登录态的系统来说简直是神器。为什么选Playwright而不是纯HTTP客户端状态共享你可以先用Playwright模拟浏览器登录获取到JWT Token或Session然后直接用同一个context去发起API请求无需手动管理Token的传递和刷新。强大的断言与等待Playwright内置了丰富的断言机制对JSON响应体的校验非常方便。一体化同一个测试框架既能做API测试又能做UI测试减少了技术栈和维护成本。报告美观Playwright Test生成的HTML报告非常直观能清晰展示每个请求和响应的详情。框架搭建核心步骤初始化项目使用npm init playwrightlatest或pip install playwright创建项目。配置全局认证在playwright.config.ts或conftest.py中编写一个全局的setup钩子完成系统登录并存储认证状态。// 示例TypeScript Playwright Test import { test as base, expect } from playwright/test; import { LoginPage } from ../pages/login-page; // 假设的Page Object export const test base.extend{ authContext: any }({ authContext: async ({ browser }, use) { // 创建一个新的浏览器上下文和页面 const context await browser.newContext(); const page await context.newPage(); const loginPage new LoginPage(page); // 登录若依系统 await page.goto(http://localhost:8080); await loginPage.login(admin, admin123); // 等待登录成功可以从storageState获取状态或直接使用context await expect(page).toHaveURL(/.*index/); // 将包含登录态的context传递给测试用例 await use(context); await context.close(); }, });编写API测试用例在测试用例中使用从authContext创建的APIRequestContext来发送请求。test(查询用户列表接口, async ({ authContext }) { // 从浏览器上下文中创建API请求上下文 const apiContext await authContext.request; // 发送GET请求 const response await apiContext.get(http://localhost:8080/system/user/list, { params: { pageNum: 1, pageSize: 10 } }); // 断言 await expect(response).toBeOK(); // 状态码2xx const body await response.json(); expect(body.code).toBe(200); // 若依返回格式通常有code、msg、data expect(body.rows).toBeInstanceOf(Array); expect(body.total).toBeGreaterThan(0); });2.2 UI自动化测试Playwright 一统天下对于若依系统的前端通常是Vue Element UIUI自动化测试主要覆盖核心业务流程如用户从登录到完成一个关键操作的完整路径。Playwright在这里的优势是压倒性的跨浏览器Chromium, Firefox, WebKit、速度快、自动等待、强大的选择器和录制工具。实操要点与避坑指南使用Page Object Model (POM) 模式这是保持UI测试代码可维护性的生命线。为若依的每个主要页面登录页、主页、用户管理页、角色管理页等创建一个Page Object类封装页面元素和操作。// pages/user-management.page.ts export class UserManagementPage { constructor(private page: Page) {} // 元素定位器 readonly addUserBtn this.page.locator(button:has-text(新增)); readonly usernameInput this.page.locator([placeholder请输入用户名称]); readonly searchBtn this.page.locator(button:has-text(搜索)); readonly firstUserEditBtn this.page.locator(tbody tr:nth-child(1) button:has-text(编辑)); // 页面操作方法 async goto() { await this.page.goto(/system/user); } async searchUser(username: string) { await this.usernameInput.fill(username); await this.searchBtn.click(); await this.page.waitForLoadState(networkidle); } async clickAddUser() { await this.addUserBtn.click(); } }处理若依特有的UI组件若依前端大量使用Element UI组件如表格、对话框、表单。Playwright定位这些元素有时需要技巧。表格操作避免使用不稳定的行索引如tr:nth-child(1)最好结合文本内容定位this.page.locator(tr:has-text(admin) .el-button--text)。对话框Element UI的对话框渲染在body末尾确保你的操作在对话框可见后进行await this.page.locator(.el-dialog__wrapper:visible).waitFor();。表单验证在点击提交前可以主动触发blur事件来让表单验证生效await inputElement.blur();。登录态处理UI测试同样可以利用Playwright的storageState功能避免每个用例都从头登录。在全局setup中登录一次将状态保存为文件后续测试直接加载。# 通过CLI工具录制登录并保存状态 npx playwright codegen --save-storageauth-state.json http://localhost:8080然后在配置中引用// playwright.config.ts use: { storageState: auth-state.json, },2.3 单元测试与集成测试JUnit 5 Mockito对于后端的Service、Controller层单元测试是保证代码质量的第一道防线。若依基于Spring Boot自然选用JUnit 5和Mockito。关键实践测试Service业务逻辑重点测试自己编写的业务方法。使用SpringBootTest进行集成测试或使用ExtendWith(MockitoExtension.class)进行纯单元测试更快。ExtendWith(MockitoExtension.class) class UserServiceImplTest { InjectMocks private UserServiceImpl userService; Mock private UserMapper userMapper; Test void testGetUserById_Success() { // Given Long userId 1L; User mockUser new User(); mockUser.setUserId(userId); mockUser.setUserName(test); when(userMapper.selectUserById(userId)).thenReturn(mockUser); // When User result userService.selectUserById(userId); // Then assertNotNull(result); assertEquals(userId, result.getUserId()); assertEquals(test, result.getUserName()); verify(userMapper, times(1)).selectUserById(userId); } }测试Controller层可以使用WebMvcTest切片测试只加载Web层相关的Bean效率更高。重点测试接口路径、参数绑定、返回格式是否符合若依的统一封装如AjaxResult。WebMvcTest(UserController.class) class UserControllerTest { Autowired private MockMvc mockMvc; MockBean private IUserService userService; Test void testListUser() throws Exception { // 模拟Service返回 ListUser userList Arrays.asList(new User(), new User()); when(userService.selectUserList(any())).thenReturn(userList); // 执行请求并验证 mockMvc.perform(get(/system/user/list) .param(pageNum, 1) .param(pageSize, 10)) .andExpect(status().isOk()) .andExpect(jsonPath($.code).value(200)) .andExpect(jsonPath($.rows, hasSize(2))); } }注意事项数据库隔离涉及数据库的测试使用Transactional和Rollback确保测试数据不污染环境。或者使用Testcontainers启动一个真实的MySQL容器但这会慢很多。依赖注入若依使用了MyBatisMapper接口需要被Mock。确保你Mock的是正确的Mapper方法。上下文配置如果测试需要完整的Spring上下文如涉及权限注解PreAuthorize则使用SpringBootTest并考虑使用TestPropertySource指定测试配置文件。3. 性能测试实战从基准测试到压力探底性能测试不是简单地用工具“跑一下”而是有明确目标的系统性工程。对于若依系统我们通常关注以下几个层面接口响应时间、系统吞吐量、并发用户支持能力、以及在高负载下的稳定性和资源消耗。工具上JMeter依然是经典且强大的选择但这里我想结合另一个工具k6来展示因为它脚本编写更简单JavaScript更适合集成到CI/CD中。3.1 性能测试场景设计首先要设计贴合若依系统实际使用场景的测试脚本。基准测试Baseline Test单用户、低并发持续运行一段时间如5-10分钟。目的是获取系统在无压力下的性能表现平均响应时间、错误率作为后续测试的对比基线。测试场景用户登录 - 进入首页 - 查询用户列表。负载测试Load Test逐步增加并发用户数如10、50、100直到达到预期的日常最大并发数。观察响应时间、吞吐量RPS和资源CPU、内存的变化曲线找到性能拐点。测试场景混合场景模拟20%的用户在新增数据80%的用户在查询数据。压力测试Stress Test在负载测试找到的拐点附近继续增加并发直到系统出现错误率飙升或响应时间不可接受如超过5秒。目的是找出系统的绝对瓶颈和最大容量。测试场景集中对某个复杂查询接口或报表生成接口进行高压冲击。稳定性测试Endurance Test以系统日常平均负载的1.5-2倍并发持续运行较长时间如8小时、24小时。目的是发现内存泄漏、连接池耗尽、数据库连接不释放等长期运行才会暴露的问题。3.2 使用 k6 编写性能测试脚本k6的脚本是ES6 JavaScript对前端开发者更友好。下面是一个针对若依系统“登录并查询用户列表”场景的k6脚本示例。// ruoyi_performance_test.js import http from k6/http; import { check, sleep, group } from k6; import { htmlReport } from https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js; // 1. 初始化选项 export const options { stages: [ { duration: 1m, target: 20 }, // 1分钟内爬升到20个虚拟用户 { duration: 3m, target: 50 }, // 3分钟内保持在50个用户 { duration: 1m, target: 0 }, // 1分钟内降回0 ], thresholds: { http_req_duration: [p(95)2000], // 95%的请求响应时间应小于2秒 http_req_failed: [rate0.01], // 请求失败率应小于1% }, }; // 全局变量用于存储登录token let authToken ; // 2. 初始化函数只运行一次 export function setup() { // 这里可以执行一些初始化操作比如获取配置 console.log(Setup: 初始化测试...); } // 3. 主测试函数每个虚拟用户都会反复执行 export default function () { // 分组用户登录 group(用户登录流程, function () { const loginUrl http://localhost:8080/login; const payload JSON.stringify({ username: admin, password: admin123, }); const params { headers: { Content-Type: application/json }, }; const loginRes http.post(loginUrl, payload, params); // 断言登录成功 check(loginRes, { 登录状态码是200: (r) r.status 200, 登录返回成功标识: (r) { const body JSON.parse(r.body); authToken body.token || body.access_token; // 根据若依实际返回字段调整 return body.code 200; }, }); // 思考时间模拟用户操作间隔 sleep(1); }); // 分组查询用户列表依赖登录token group(查询用户列表, function () { // 若依的列表查询接口通常需要认证 const listUrl http://localhost:8080/system/user/list?pageNum1pageSize10; const params { headers: { Authorization: Bearer ${authToken}, // 或 X-Token: authToken Content-Type: application/json, }, }; const listRes http.get(listUrl, params); check(listRes, { 查询状态码是200: (r) r.status 200, 查询返回数据有效: (r) { const body JSON.parse(r.body); return body.code 200 Array.isArray(body.rows); }, }); sleep(0.5); }); } // 4. 收尾函数只运行一次可用于生成报告 export function teardown(data) { console.log(Teardown: 测试结束); }执行与结果分析# 运行测试 k6 run ruoyi_performance_test.js # 输出结果会包含大量指标重点关注 # http_req_duration (请求耗时)avg, p(90), p(95) # http_reqs (吞吐量)每秒请求数 # iteration_duration (迭代耗时)每个虚拟用户完成一次默认函数的耗时 # vus (虚拟用户数)分析结果时要结合阈值thresholds判断。如果p(95)响应时间超过2秒或者错误率超过1%就需要深入排查。3.3 性能瓶颈分析与调优实战当k6或JMeter报告显示性能不达标时我们需要一套排查组合拳。以下是我在实际项目中排查若依系统性能问题的常用路径应用服务器Tomcat/Spring Boot工具Arthas、Spring Boot Actuator/actuator/metrics,/actuator/health。查什么线程池状态tomcat.threads.busy、JVM内存堆、非堆、GC频率和耗时、慢SQL如果集成了Druid可用其监控台。常见问题与调优线程池打满调整server.tomcat.max-threads默认200根据CPU核心数设置通常建议在50-200之间。频繁Full GC使用jstat -gcutil pid观察。优化JVM参数如-Xmx,-Xms设置相同避免动态调整使用G1垃圾回收器-XX:UseG1GC。若依特定配置检查ruoyi.xx相关配置如异步任务线程池大小ruoyi.async.pool.size、Excel导出导入的缓存设置。数据库MySQL工具EXPLAIN、SHOW PROCESSLIST、慢查询日志。查什么执行计划是否走索引、是否存在全表扫描、锁等待情况。常见问题与调优若依通用查询若依的BaseEntity包含createTime,updateTime等字段业务表关联查询时务必在这些字段和常用查询条件上建立索引。分页查询慢LIMIT M, N在M很大时效率极低。考虑使用WHERE id last_id LIMIT N的方式优化或者使用覆盖索引。连接池使用HikariCP配置spring.datasource.hikari.maximum-pool-size建议等于或略大于应用服务器最大线程数connection-timeout设置合理。前端Vue Nginx工具浏览器开发者工具Network, Performance面板、Lighthouse。查什么首屏加载时间、资源文件JS/CSS大小、接口请求耗时。常见问题与调优打包体积过大使用npm run build -- --report分析包体积按需引入Element-UI等组件库使用CDN加载公共库vue, vuex, vue-router。接口请求慢除了后端优化前端可考虑防抖/节流、请求合并、数据缓存如vuex-persistedstate。Nginx配置开启gzip压缩、配置静态资源缓存、优化keepalive_timeout等参数。一个真实的调优案例我们曾遇到若依系统用户列表页在数据量超过10万时查询极慢。通过Arthas的trace命令追踪发现耗时主要在MyBatis的selectUserList方法。用EXPLAIN分析SQL发现虽然user_id有索引但联表查询sys_dept时dept_id上没有索引。建立索引后查询时间从5秒降到了50毫秒。同时我们发现前端表格是一次性加载所有数据改为后端分页后体验大幅提升。4. 集成到CI/CD让测试自动运转起来自动化测试和性能测试如果不纳入持续集成流程其价值就会大打折扣。我们的目标是代码提交触发自动化测试每日构建运行性能基准测试。4.1 使用Jenkins Pipeline编排测试流水线以下是一个简化的Jenkinsfile示例展示了如何将单元测试、接口自动化测试、UI自动化测试和性能测试串联起来。pipeline { agent any tools { maven Maven-3.8 nodejs Node-16 } stages { stage(代码检出) { steps { git branch: main, url: https://your-git-repo.git } } stage(后端构建与单元测试) { steps { sh mvn clean compile test // 运行JUnit测试 } post { always { junit target/surefire-reports/*.xml // 收集测试报告 } } } stage(前端构建) { steps { dir(ruoyi-ui) { sh npm install sh npm run build:prod } } } stage(接口自动化测试 (Playwright)) { steps { dir(test/playwright-api) { sh npm install sh npx playwright install --with-deps sh npx playwright test --reporterhtml,line // 运行API测试 } } post { always { // 归档Playwright HTML报告 archiveArtifacts artifacts: test/playwright-api/playwright-report/**, fingerprint: true // 如果测试失败发送通知 // emailext ... } } } stage(UI自动化测试 (Playwright)) { steps { dir(test/playwright-ui) { sh npm install sh npx playwright install --with-deps // 先启动被测应用然后运行测试 sh nohup java -jar your-ruoyi-app.jar app.log 21 APP_PID$! sleep 30 # 等待应用启动 npx playwright test --reporterhtml,line kill $APP_PID } } post { always { archiveArtifacts artifacts: test/playwright-ui/playwright-report/**, fingerprint: true } } } stage(性能基准测试 (k6)) { steps { script { // 使用Docker运行k6确保环境一致 sh docker run --rm -v $(pwd)/k6-scripts:/scripts grafana/k6 run /scripts/ruoyi_performance_test.js } } post { always { // k6输出是控制台可以将其重定向到文件并归档或集成到Grafana sh docker run --rm -v $(pwd)/k6-scripts:/scripts -v $(pwd)/results:/results grafana/k6 run --out json/results/result.json /scripts/ruoyi_performance_test.js archiveArtifacts artifacts: results/*.json } } } stage(部署到测试环境) { when { expression { currentBuild.resultIsBetterOrEqualTo(SUCCESS) } } steps { // 你的部署脚本如通过SSH、K8s等 echo 部署中... } } } post { failure { // 构建失败通知 emailext body: 项目${JOB_NAME}构建失败请检查\n详情${BUILD_URL}, subject: 构建失败通知: ${JOB_NAME}, to: teamexample.com } } }4.2 关键配置与避坑经验测试环境隔离自动化测试尤其是UI测试需要一个干净的、可预测的测试环境。使用Docker Compose一键启动整套环境MySQL Redis 后端应用 前端Nginx是最佳实践。确保每次测试前数据库都有固定的初始数据可以通过Flyway或Liquibase维护测试数据脚本。等待与超时UI自动化测试失败十有八九是因为“等待”。Playwright的auto-waiting机制很好但对于某些复杂的动态加载如若依表格数据通过Ajax加载可能需要显式等待await page.waitForResponse(response response.url().includes(/list) response.status() 200)。测试数据管理自动化测试不能依赖生产数据也不能让测试用例相互影响。每个测试用例应该创建自己需要的数据并在测试后清理BeforeEach和AfterEach钩子。对于接口测试可以使用Transactional回滚对于UI测试可以调用专门的测试清理接口。报告与通知Playwright HTML报告、JUnit XML报告、k6的输出结果都要妥善归档。可以将这些报告发布到内部站点或集成到如Allure TestOps、Grafana等平台进行可视化。构建失败时及时通过邮件、钉钉、企业微信通知负责人。性能测试的稳定性性能测试对环境极其敏感。确保性能测试环境与生产环境配置尽可能接近尤其是CPU、内存、数据库配置。每次性能测试前重启应用和数据库确保从一个干净的状态开始。运行多次取平均值以减少偶然误差。5. 常见问题与排查技巧实录在实际操作中你会遇到各种各样稀奇古怪的问题。这里记录几个我和团队踩过的坑以及我们的解决办法。问题一Playwright UI测试中点击若依的Element UI下拉框Select没反应。现象代码定位到了下拉框并执行了click()但下拉菜单没有弹出。排查Element UI的下拉框并非原生select而是由div和input模拟的。直接点击触发元素可能不对。解决先点击触发下拉框展开的元素await page.locator(.el-select .el-input__inner).click();等待下拉选项出现await page.locator(.el-select-dropdown:visible).waitFor();再点击下拉选项await page.locator(.el-select-dropdown__item:has-text(选项文本)).click();更稳健的写法使用Playwright的selectOption方法但它只对原生select有效。对于Element UI可以封装一个自定义函数来模拟完整操作。问题二接口自动化测试时若依返回的Token过期如何处理现象测试运行一段时间后因Token过期导致后续所有需要认证的接口失败。解决短期方案测试脚本内在Playwright的测试中监听接口返回的401状态码一旦出现则重新执行登录流程更新全局的authToken。这可以通过在beforeEach或setup中增加Token有效性检查来实现。长期方案架构层面在测试环境中配置一个较长的Token过期时间如24小时。或者使用若依的“测试账号”特性该账号的Token永不过期需后端支持不推荐用于安全要求高的环境。问题三性能测试中TPS每秒事务数上不去但服务器CPU和内存使用率都很低。现象并发用户数增加但TPS不增长响应时间却线性增加。服务器监控显示资源空闲。排查思路检查应用线程池可能是Tomcat或应用内自定义线程池已满新的请求在队列中等待。通过/actuator/metrics或JMX查看tomcat.threads.busy。检查数据库连接池连接池maxActive设置过小导致大量数据库请求在等待获取连接。查看Druid监控或相关日志。检查外部依赖系统是否调用了外部慢接口如短信网关、支付接口使用trace工具追踪调用链。检查锁竞争是否存在数据库行锁、表锁或应用内的同步锁synchronized高并发下锁竞争会严重限制吞吐量。检查压力机本身运行k6或JMeter的机器性能是否足够网络带宽是否成为瓶颈监控压力机的CPU、内存、网络IO。我们的案例一次压力测试中发现TPS卡在100左右。排查后发现是Redis连接池配置过小默认8大量请求在等待Redis连接。将lettuce.pool.max-active调整为50后TPS立刻提升到300。问题四若依系统导出Excel功能在高并发下内存溢出OOM。现象当多个用户同时触发大数据量导出时应用频繁Full GC最终OOM崩溃。分析若依使用的EasyExcel或Apache POI在导出时会生成大量对象驻留内存。并发导出时内存峰值叠加极易撑爆堆内存。解决方案限流在导出接口上添加限流如使用Sentinel控制同一时刻的导出请求数。异步导出改造导出功能为异步任务。用户点击导出后后端生成任务放入消息队列如RocketMQ由后台线程处理生成文件后存储到OSS或服务器前端轮询或通过WebSocket通知用户下载。这彻底解耦了请求与耗时操作。优化导出逻辑流式读取数据库分页处理避免一次性加载全部数据到内存。增大JVM堆内存临时方案治标不治本。-Xmx设置为物理内存的50%-70%。问题五自动化测试用例在CI/CD中不稳定时而成功时而失败。现象本地运行稳定的测试在Jenkins上偶尔失败错误多是“元素未找到”或“超时”。排查与解决环境差异CI环境与本地环境的网络、硬件、软件版本浏览器、驱动是否一致使用Docker镜像固化测试环境。资源竞争CI机器上是否并行运行了多个任务导致资源不足给Jenkins Agent分配足够的CPU和内存并控制任务并发度。等待策略CI环境可能比本地慢。增加全局的timeout配置或使用更智能的等待条件如waitForSelector配合state: visible而非固定的sleep。测试隔离确保每个测试套件test suite甚至每个测试用例test case都是独立的不依赖其他测试留下的数据或状态。使用数据库事务回滚或独立的测试数据。截图与录像在测试失败时自动截取页面截图和保存操作录像。Playwright原生支持video: on和trace: on这在CI中排查问题至关重要。