若依(RuoYi-Vue)自动化测试踩坑实录:从 0 到 27 个用例全过的血泪史
🧪
一个研二转测试开发的菜鸟,花了三天把若依后台管理系统的自动化测试搭起来。记录一下踩过的坑,希望能帮到同样在学测试的同学。
一、为什么要做这个项目
先交代下背景:本人研二,图像识别方向,正在学 Java 测试开发准备找暑期实习。之前已经用 Selenium + REST Assured 做过一个商城项目的自动化测试,简历上只有一个项目经历有点单薄,于是盯上了 若依(RuoYi-Vue)——这个在 Java 圈几乎人手一个的开源后台管理系统。
选它的原因很简单:
- 技术栈匹配(Spring Boot + Vue + MySQL + Redis),跟企业真实项目高度一致
- 有完整的 RBAC 权限模型,能写出有深度的权限边界测试用例
二、技术选型 & 项目结构
先放一张整体架构图(用 Docker Compose 一键部署):
📦 Docker Compose 测试环境(docker compose up -d 一键启动)
├── 🗄️ MySQL 8.0 (:3306)
│ └── 启动时自动执行 init.sql 建表 + 导入初始数据
│
├── ⚡ Redis 7 (:6379)
│ └── 缓存用户权限、存储验证码
│
├── ☕ RuoYi Server (:8080)
│ └── Spring Boot 4.0 + JWT 无状态鉴权
│ └── 依赖 MySQL + Redis(健康检查通过后才启动)
│
├── 🌐 RuoYi UI (:80)
│ └── Vue3 + Element Plus,Nginx 反向代理
│ └── 依赖 RuoYi Server
│
└── 🧪 测试层(独立 Maven 项目)
├── api-tests/ → REST Assured + TestNG,27 个用例
└── ui-tests/ → Selenium + Page Object,8 个用例
| 组件 | 选型 | 原因 |
|---|---|---|
| 测试框架 | REST Assured + TestNG | REST Assured 的 Given/When/Then 语法太优雅了,TestNG 的 @DataProvider 做数据驱动比 JUnit 方便 |
| 断言库 | AssertJ + Hamcrest | AssertJ 的流式断言可读性爆表,Hamcrest 配合 REST Assured 的 body() 校验 |
| 报告 | Allure | 可视化报告,面试加分项 |
| 环境 | Docker Compose | 一键启动,测试环境一致性有保障 |
项目已经开源在 Gitee 上:ruoyi-test-automation,clone 下来配好 Docker 就能跑。
三、踩坑实录(这才是本文的重点)
别看我上面说得头头是道,实际搞的时候踩了无数坑。下面按时间线复盘,每一个都是真金白银换来的经验。
🔥 坑 1:Docker 网络——搞了两个小时
现象:docker compose up -d 一直卡在 pull 镜像,然后超时。
第一反应:我网络有问题?ping 一下百度是通的啊。
排查过程:
# 发现 docker pull 走的是 Docker Hub(auth.docker.io),国内直连被墙
$ docker pull mysql:8.0
Error response from daemon: Get https://registry-1.docker.io/v2/: dial tcp: connect: connection refused
解决方案(血泪经验):
- ❌ 不要用镜像加速器(DaoCloud 的
docker.m.daocloud.io已经不稳定了,经常 401) - ✅ 走代理,但 Docker Desktop GUI 配的代理不一定生效!
- ✅ 终极方案:命令行直接 export 环境变量
export HTTP_PROXY="http://127.0.0.1:7897"
export HTTPS_PROXY="http://127.0.0.1:7897"
docker compose up -d
💡 教训:Docker Desktop 的 Settings → Resources → Proxies 配置有时候就是个摆设,环境变量才是最稳的。
🔥 坑 2:端口冲突——Windows 原生 MySQL 抢了 3306
现象:docker compose up -d 报错 port 3306 already in use。
排查:
# PowerShell 查端口占用
Get-NetTCPConnection -LocalPort 3306 | Select LocalAddress, OwningProcess
# 发现是一个叫 mysqld.exe 的进程
# 查进程路径
Get-Process -Id 6572 | Select Id, ProcessName, Path
# C:\Program Files\MySQL\MySQL Server 5.7\bin\mysqld.exe
我去! Windows 自己装了个 MySQL 5.7,开机自启,占着 3306 端口。WSL 里还有一个 MySQL 8.0 也在跑……
解决:
# 停掉 Windows 的 MySQL
Stop-Service MySQL57 -Force
# 停掉 WSL 里的 MySQL 和 Redis
wsl -d Ubuntu-24.04 -u root -- service mysql stop
wsl -d Ubuntu-24.04 -u root -- service redis-server stop
💡 教训:Docker 之前先检查本地服务有没有端口冲突,Windows 的
netstat -ano和 PowerShell 的Get-NetTCPConnection是排查利器。
🔥 坑 3:验证码——登录一直 500,差点怀疑人生
现象:测试用例跑起来,登录接口返回 {"code": 500},所有后续测试全挂。
排查过程:
先看日志发现是验证码校验失败。但我在 BaseApiTest.login() 里已经发了验证码请求啊:
// 原来的代码——少传了 code 和 uuid!
Map<String, String> loginBody = new HashMap<>();
loginBody.put("username", username);
loginBody.put("password", password);
// 没有 code 和 uuid!
我以为验证码是摆设,结果若依的 captcha 是默认开启的,而且类型是 math(数学运算验证码)。数据库里 sys.account.captchaEnabled 默认是 true。
解决方案(终极):
# 直接改数据库关掉验证码——测试环境没必要折磨自己
docker exec ruoyi-mysql mysql -uroot -proot123 ruoyi \
-e "INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark)
VALUES ('验证码开关', 'sys.account.captchaEnabled', 'false', 'Y', 'admin', NOW(), '测试环境关闭验证码');"
然后在 BaseApiTest.login() 里加上自动获取 captcha uuid 的逻辑:
protected void login(String username, String password) {
// Step 1: 获取验证码 uuid(关掉验证码后这一步其实多余,但保留以防万一)
String captchaUuid = "";
try {
Response captchaResp = given().get("/captchaImage");
captchaUuid = captchaResp.jsonPath().getString("uuid");
} catch (Exception e) {
// 验证码已关闭时忽略
}
// Step 2: 构造完整登录请求
Map<String, Object> loginBody = new HashMap<>();
loginBody.put("username", username);
loginBody.put("password", password);
loginBody.put("code", "1234"); // 测试环境不校验
loginBody.put("uuid", captchaUuid);
Response response = given()
.contentType(ContentType.JSON)
.body(loginBody)
.post("/login");
// ...
}
💡 教训:测登录之前先手动 curl 一下
/captchaImage看captchaEnabled是 true 还是 false。不要想当然以为验证码是关的。
🔥 坑 4:docker-compose 环境变量名不匹配 Druid 数据源
现象:后端容器连不上 MySQL,日志刷屏 Communications link failure。
原因:若依用的是 Druid 连接池,配置路径是嵌套的 spring.datasource.druid.master.url,而不是 Spring Boot 标准的 spring.datasource.url。
我一开始在 docker-compose.yml 里写的是:
# ❌ 错误写法
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://ruoyi-mysql:3306/ruoyi?...
但 Druid 的配置路径根本不吃这套!环境变量映射规则是:
SPRING_DATASOURCE_URL→spring.datasource.url(标准 Spring Boot)SPRING_DATASOURCE_DRUID_MASTER_URL→spring.datasource.druid.master.url(Druid)
修复:
# ✅ 正确写法
environment:
SPRING_DATASOURCE_DRUID_MASTER_URL: jdbc:mysql://ruoyi-mysql:3306/ruoyi?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
SPRING_DATASOURCE_DRUID_MASTER_USERNAME: root
SPRING_DATASOURCE_DRUID_MASTER_PASSWORD: root123
SPRING_DATA_REDIS_HOST: ruoyi-redis # 注意是 SPRING_DATA_REDIS 不是 SPRING_REDIS!
这里还有一个子坑:Redis 的配置是 spring.data.redis.host,不是 spring.redis.host,所以环境变量名必须用 SPRING_DATA_REDIS_HOST。
💡 教训:Spring Boot 的 relaxed binding 规则是
点号→下划线→大写→环境变量名。遇到嵌套配置(如 Druid、Redis),一定要看application.yml里的实际属性路径,不然环境变量压根不生效。
🔥 坑 5:编译错误三连——boolean != null、equalTo 未导入、XML 的 &
现象:mvn test 直接编译失败,报了 3 个错。
错误 1:boolean != null
// ❌ 编译错误:getBoolean() 返回基本类型 boolean,不能和 null 比较
if (resp.jsonPath().getBoolean("captchaEnabled") != null) {
这种低级错误……写的时候脑子抽了属于是。getBoolean() 返回的是 boolean(基本类型),不存在 null。
// ✅ 改用 getObject() 返回包装类 Boolean
if (resp.jsonPath().getObject("captchaEnabled", Boolean.class) != null) {
错误 2:equalTo(int) 方法找不到
排查半天才发现是 SysRoleTest.java 忘了 import org.hamcrest.Matchers.equalTo。之前从 LoginTest.java 复制代码过来漏了静态导入。
错误 3:TestNG XML 的 & 没转义
<!-- ❌ XML 解析失败 -->
<test name="System Module - Role & RBAC">
<!-- ✅ & 必须写成 & -->
<test name="System Module - Role & RBAC">
这个坑我踩了两次了……XML/HTML 里 & 必须转义成 &,属于是每次都会忘的老毛病。
💡 教训:IDEA 的代码检查能发现前两个错,第三个纯属自己不小心。写完代码
mvn compile先跑一遍再写测试,别跟我似的写完一堆测试才发现编译都过不了。
🔥 坑 6:测试数据冲突——邮箱/手机号重复导致创建用户 500
现象:SysUserTest.testCreateUser 总是 500,但用 curl 单独发同样的请求就 200。
排查过程:curl 能过说明接口没问题,那肯定是我测试代码的问题。加了一堆 System.out.println 后发现:testCreateDuplicateUser(同优先级)在 testCreateUser 之前跑了,先用某个邮箱创建了用户,然后 testCreateUser 又用同一个邮箱创建,触发了唯一约束。
根因在于 buildTestUser() 方法:
// ❌ 有 bug 的写法
private Map<String, Object> buildTestUser(String userName) {
user.put("email", "test" + testUserSuffix + "@test.com"); // 所有用户共用一个 suffix!
user.put("phonenumber", "138" + testUserSuffix); // 手机号也是!
}
testUserSuffix 是 System.currentTimeMillis() % 100000,同一个测试类里所有方法共用同一个值。第一个测试创建了 dup_test_12345,邮箱是 test12345@test.com;然后 testCreateUser 创建 test_user_12345,邮箱也是 test12345@test.com → 炸了。
// ✅ 修复:每个用户生成唯一邮箱/手机号
private Map<String, Object> buildTestUser(String userName) {
String uid = userName != null ? userName : "test_" + testUserSuffix;
String uniqueSuffix = uid.replaceAll("[^a-zA-Z0-9_]", "_");
user.put("email", uniqueSuffix + "@test.com");
user.put("phonenumber", "138" + String.valueOf(System.nanoTime() % 100000000));
}
💡 教训:测试数据构造方法里不要用实例级别的共享变量来生成唯一字段。要么用
System.nanoTime()这种每次调用都不同的值,要么把userName本身编码进邮箱/手机号里。
🔥 坑 7:权限测试的"静默回退"——测试骗过了自己
现象:RBAC 权限边界测试 testNormalUserCannotCreateRole 一直失败——普通用户居然能创建角色??我以为若依有安全漏洞。
排查过程:顺着 @PreAuthorize 注解一路追踪源码,确认了:
SysRoleController.add()需要system:role:add权限- 角色 ID 2(普通角色)只有
system:role:list,没有system:role:add - 理论上普通用户应该被拦截
那为什么测试显示他能创建角色?问题出在我写的辅助方法上:
// ❌ 这个 fallback 直接毁了权限测试!
private String createAndLoginNormalUser(String userName) {
// ...创建普通用户...
if (loginResp.jsonPath().getInt("code") == 200) {
return loginResp.jsonPath().getString("token");
}
// 🔥 这里!用户创建失败就静默切回 admin token!
System.out.println("[WARN] Normal user login failed, using admin token as fallback");
loginAsAdmin();
return cachedToken; // ← 这是 admin 的 token!
}
普通用户创建失败了(因为邮箱/手机号冲突——没错,又是坑 6),然后静默回退到了 admin token,再用 admin token 去测试"普通用户能不能创建角色"——当然能!测试骗过了自己。
// ✅ 修复:不容忍回退,登录失败直接让测试失败
if (loginResp.jsonPath().getInt("code") == 200) {
return loginResp.jsonPath().getString("token");
}
// 不静默回退,直接暴露问题
String msg = "[RBAC] Normal user login failed: " + loginResp.body().asString();
assertThat(false).as(msg).isTrue();
return null;
💡 教训:这是我觉得最有价值的一个坑。测试辅助方法的 fallback 逻辑如果不严谨,可能导致"假阳性"——测试全绿但测的根本不是你想测的东西。 异常场景就该让它炸,不要偷偷用 fallback 掩盖问题。
四、最终成果
测试执行结果
Tests run: 27, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
测试覆盖矩阵
| 模块 | 测试类 | 用例数 | 覆盖场景 |
|---|---|---|---|
| 登录 | LoginTest | 7 | 正向登录、空用户名/密码、错误密码、SQL注入防御、未授权访问 |
| 用户管理 | SysUserTest | 8 | CRUD 全链路、分页边界、唯一约束、状态变更 |
| 角色管理 | SysRoleTest | 8 | CRUD、RBAC 权限边界(普通用户不能删除角色)、数据权限隔离、伪造 Token |
| 字典管理 | SysDictTypeTest | 4 | 列表查询、新增(含正常和重复校验) |
收官总结
- Docker 环境一致性:
docker compose up -d一键拉起,杜绝"我机器上能跑" - RBAC 权限边界测试:验证了横向越权——普通用户不能通过直接调 API 来访问管理功能
- 数据驱动测试:
@DataProvider实现登录失败 5 组数据一行不改 - 全链路测试:增→查→改→删→查确认删除,每个模块都是完整闭环
五、给同样在学测试的初学者的建议
-
先手动 curl 把接口调通,再写测试代码。 如果接口本身就有问题,你写再多测试也是红的,分不清是代码 bug 还是环境问题。
-
测试辅助方法要谨慎设计 fallback。 如果辅助方法静默掩盖了异常,你的测试就是自欺欺人。
-
Docker 环境是测试的基础,但不是银弹。 我花了整整一天搞 Docker 网络、端口冲突、环境变量匹配……但这都是值得的,因为搭好之后再也用担心环境问题。
-
代码写完先
mvn compile,别跟我似的。 编译不过的代码写再多测试都是白搭。 -
遇到 500 别急着怀疑接口,先看服务器日志。
docker logs ruoyi-server是我这个项目用得最多的命令,没有之一。 -
测试数据一定要唯一。 用
System.currentTimeMillis()加随机后缀,不然数据冲突找 bug 找到怀疑人生。
项目源码在 Gitee:https://gitee.com/fu-yu-bin/ruoyi-test-automation
环境要求:Docker Desktop + Java 17 + Maven 3.9,clone 下来照着 README 走就行。
更多推荐



所有评论(0)