🧪

一个研二转测试开发的菜鸟,花了三天把若依后台管理系统的自动化测试搭起来。记录一下踩过的坑,希望能帮到同样在学测试的同学。


一、为什么要做这个项目

先交代下背景:本人研二,图像识别方向,正在学 Java 测试开发准备找暑期实习。之前已经用 Selenium + REST Assured 做过一个商城项目的自动化测试,简历上只有一个项目经历有点单薄,于是盯上了 若依(RuoYi-Vue)——这个在 Java 圈几乎人手一个的开源后台管理系统。

选它的原因很简单:

  1. 技术栈匹配(Spring Boot + Vue + MySQL + Redis),跟企业真实项目高度一致
  2. 有完整的 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

解决方案(血泪经验):

  1. 不要用镜像加速器(DaoCloud 的 docker.m.daocloud.io 已经不稳定了,经常 401)
  2. 走代理,但 Docker Desktop GUI 配的代理不一定生效!
  3. 终极方案:命令行直接 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 一下 /captchaImagecaptchaEnabled 是 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_URLspring.datasource.url(标准 Spring Boot)
  • SPRING_DATASOURCE_DRUID_MASTER_URLspring.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">

<!-- ✅ & 必须写成 &amp; -->
<test name="System Module - Role &amp; RBAC">

这个坑我踩了两次了……XML/HTML 里 & 必须转义成 &amp;,属于是每次都会忘的老毛病。

💡 教训: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);              // 手机号也是!
}

testUserSuffixSystem.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 组数据一行不改
  • 全链路测试:增→查→改→删→查确认删除,每个模块都是完整闭环

五、给同样在学测试的初学者的建议

  1. 先手动 curl 把接口调通,再写测试代码。 如果接口本身就有问题,你写再多测试也是红的,分不清是代码 bug 还是环境问题。

  2. 测试辅助方法要谨慎设计 fallback。 如果辅助方法静默掩盖了异常,你的测试就是自欺欺人。

  3. Docker 环境是测试的基础,但不是银弹。 我花了整整一天搞 Docker 网络、端口冲突、环境变量匹配……但这都是值得的,因为搭好之后再也用担心环境问题。

  4. 代码写完先 mvn compile,别跟我似的。 编译不过的代码写再多测试都是白搭。

  5. 遇到 500 别急着怀疑接口,先看服务器日志。 docker logs ruoyi-server 是我这个项目用得最多的命令,没有之一。

  6. 测试数据一定要唯一。System.currentTimeMillis() 加随机后缀,不然数据冲突找 bug 找到怀疑人生。


项目源码在 Gitee:https://gitee.com/fu-yu-bin/ruoyi-test-automation

环境要求:Docker Desktop + Java 17 + Maven 3.9,clone 下来照着 README 走就行。

更多推荐