ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

软件测试课程设计实战:MySQL环境、JUnit用例与LoadRunner性能

软件测试课程设计实战:MySQL环境、JUnit用例与LoadRunner性能

简介:面向高校软件工程、计算机相关专业的课程设计场景,围绕JavaWeb网上书店前台系统展开测试实践,适合正在完成软件测试课程设计、需要串联白盒与黑盒测试方法的读者使用。压缩包共904个文件、约45.97MB,内含jsp页面、java源码、class与jar依赖、xml配置、css/js脚本及gif/jpg/png界面素材,较完整地保留了IDEA下可运行、可测试的Web项目结构。已有2350人学习下载,在课程设计选题与测试用例组织上具有一定参考意义。内容覆盖逻辑覆盖与基本路径覆盖的用例设计,结合等价类、边界值分析完成黑盒测试,并给出JUnit单元测试、功能测试以及LoadRunner稳定性与压力测试思路,读者可对照核心函数、页面流程和负载场景,理解从测试需求分析到用例落地、再到性能指标观察的完整过程,也可作为课程报告与答辩准备的样例。

1. 三周做完一份软件测试课程设计,卡点从来不在写用例

期末前两周,某开发者把一份软件测试课程设计.zip丢给我,说他卡了三天:被测的 JavaWeb 系统能启动,但登录页一直报 500;LoadRunner 录制的脚本回放全部失败,报告里的 TPS 曲线是断的。我打开压缩包看了一遍,问题其实很朴素:他先动手写用例,没先把数据库和字符集对齐。

这份软件测试课程设计资源把四样东西装在一个包里:一套能启动的 JavaWeb + MySQL 被测系统、一份建库脚本、一套按模块拆开的测试用例与缺陷记录、一组 LoadRunner 性能脚本。它适合两类人:一是课程设计要交功能测试用例、缺陷报告和性能报告的在校开发者;二是想拿一套完整的 JavaWeb 小系统练 JUnit 和 LoadRunner 的转行同学。

2. 资源全景:目录结构、技术栈与四份交付物

拿到压缩包先别右键“导入项目”。我一般的习惯是先展开目录树,确认被测系统、数据库脚本、测试资产、性能脚本这四块齐不齐。缺哪一块,后面的活都会返工:缺 data.sql 就得自己造数据,用例跑出来全是假失败;缺 LoadRunner 脚本,性能报告就只剩一段手写文字。

2.1 目录结构:被测系统、测试资产、性能脚本三段切分

解压后大致是下面这个形态,不同版本细节会有出入,但骨架基本一致:

软件测试课程设计/ ├── 01_被测系统/ # 被测对象,一个能跑起来的 JavaWeb 工程 │ ├── pom.xml # Maven 依赖,看这里判断 JDK 和框架版本 │ ├── src/main/java/ # controller / service / dao / util 分层 │ ├── src/main/webapp/ # JSP 页面、WEB-INF/web.xml、静态资源 │ └── src/test/java/ # 少量 JUnit 示例,可扩展 ├── 02_数据库/ │ ├── schema.sql # 建表语句 │ └── data.sql # 初始数据,缺了它用例全失败 ├── 03_测试资产/ │ ├── 用例/ 登录、注册、订单查询.xlsx │ ├── 缺陷/ 缺陷记录表.xlsx │ └── 报告/ 功能测试报告、性能测试报告.docx ├── 04_性能测试/ │ ├── script/ # LoadRunner 脚本目录,含 .usr 与 Action 文件 │ └── scenario/ # 场景配置文件与压测数据参数文件 └── README.md # 环境要求和启动步骤,先读它

四块各自承担一个交付目标。01_被测系统是被测对象,也是功能用例的落点,你写的每条用例都得能在这里找到对应的请求入口;02_数据库决定用例能不能跑出真实结果,很多“密码错误提示不对”的坑,追下来其实是初始数据里根本没有这个账号;03_测试资产是能直接改成自己名字交上去的部分,用例编号、设计方法、预期结果都按表填好了;04_性能测试是加分项,也是多数人翻车的地方。

需要提醒一句:03_测试资产里的用例表通常按页面模块组织,不是按代码模块。对照的时候要自己建立映射,比如“订单查询”用例对应的可能是OrderController.list()加上OrderService.queryByPage()两个方法,中间还有一层分页参数处理,这些在文档里不会写。

2.2 技术栈:JavaWeb + MySQL + IDEA 这套组合的取舍

从pom.xml和webapp目录就能看出这套系统的技术选型,它走的是一条很典型的 JavaWeb 路线,而不是 Spring Boot 一条命令起服务的那种:

层次技术在测试里的落点
视图层JSP + 少量原生 JS表单元素定位、前端校验的边界用例
控制层Servlet 或轻量 Controller参数校验、跳转逻辑、HTTP 状态码
业务层Service 类JUnit 单元测试主要打这一层,跑得快、不依赖容器
持久层JDBC + MySQL数据库断言、脏数据清理、并发下的行锁
构建与运行Maven + Tomcat环境复现,避免“我这能跑”的扯皮
性能工具LoadRunner录制 HTTP 脚本、参数化、场景加压

为什么课程设计偏爱这套组合?因为它把“可观测”做得很直白。Spring Boot 里一个注解就把请求拦下来了,学生看不到过滤器链;而 Servlet 项目里web.xml、Filter、JDBC 连接都是明面上的,写用例时能清楚知道一个请求在哪个环节被拦。LoadRunner 录制走的也是标准 HTTP,这种纯表单提交、无大量前端框架拦截的页面,录制出来的脚本干净,关联点少,适合第一次接触性能工具。

我一般会先打开pom.xml确认两件事:一是 JDK 目标版本,二是 MySQL 驱动是mysql-connector-java的哪个大版本。驱动 8.x 和 5.x 的连接串参数不一样,serverTimezone这个参数在 5.x 上写了不报错,在 8.x 上不写就直接抛时区异常。这个细节后面第 5 章还会提到。

2.3 环境清单:版本对齐比装得快更重要

环境这块最容易出现“装了一整天,一个用例没跑”。按下面这份清单逐项确认,比盲目重装快得多:

组件建议版本关键点
JDK8 或 11,以 pom 的 source 为准高版本 JDK 编译低版本项目,注意--release
Maven3.6 以上配好国内镜像,否则依赖下到超时
Tomcat8.5 或 910 以上把javax.servlet换成jakarta,老项目直接跑不起来
MySQL5.7 或 8.08.0 默认字符集已经是 utf8mb4,5.7 要手动指定
IDEA社区版够用社区版没有内置 Tomcat 集成,得用 Smart Tomcat 插件
LoadRunner与操作系统位数匹配Windows 上装,注意许可证只覆盖到指定 VU 数

端口也要提前规划:MySQL 默认 3306,Tomcat 默认 8080。如果本机已经跑了别的服务,改端口比重装快。我把这些写进README.md的习惯是从一次答辩现场养成的——评委让现场演示,机器上 8080 被另一个项目占了,页面一直 404,场面很被动。

3. 跑通链路:MySQL 建库、IDEA 导入、Tomcat 起服务到第一个用例通过

这一章按执行顺序走一遍:先让数据落地,再让服务起来,然后用三个动作冒烟,最后跑通第一个自动化用例。顺序不能换,先用 JUnit 再去建库,会得到一堆NullPointerException,排查起来毫无头绪。

3.1 建库与初始化:schema 和 data 谁先跑

建库看着简单,但顺序和字符集是两个高频坑。schema.sql负责建表结构,data.sql负责灌初始数据,顺序反了就是一堆“表不存在”。字符集一定要在CREATE DATABASE时就定死,后面再改表字符集会留下隐患。

-- 建库时就把字符集定死,能省掉后面一大半乱码问题 CREATE DATABASE IF NOT EXISTS test_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE test_demo; -- 先结构后数据,顺序不能反 SOURCE schema.sql; SOURCE data.sql; -- 跑完先核对行数,数据没进来,后面所有用例都是假失败 SELECT 't_user' AS table_name, COUNT(*) AS rows_cnt FROM t_user UNION ALL SELECT 't_order', COUNT(*) FROM t_order;

SOURCE是 MySQL 客户端命令,不是 SQL 语句,所以要在命令行里执行,或者在 Navicat、DataGrip 的查询窗口里用source的等价菜单。执行完必须核对行数:t_user里至少有管理员账号,t_order里至少有几条能被查询到的订单。我做这一步时会顺手SELECT * FROM t_user LIMIT 5,确认密码字段存的是明文还是哈希——如果用例表里写的是明文密码,而库里存的是 MD5,那第一条登录用例就注定失败,处理办法要么改数据,要么在用例里补一步加密,别去改业务代码,那是被测对象。

注意:schema.sql里如果带DROP TABLE IF EXISTS,重复执行会清掉你后来手工插的测试数据。第二次跑之前先确认一下,或者把测试数据单独放一个data_test.sql。

3.2 IDEA 导入与 Tomcat 配置:Artifact 没配对就是 404

导入这一步,Maven 项目用File → Open选中pom.xml所在的目录,让 IDEA 自己解析依赖,别用New Project from Existing Sources一步步选,那样容易漏掉 resources 目录。依赖拉完后先执行一次编译:

# 先确认能不能编译通过,编译不过就别谈部署 mvn -DskipTests clean package # 看 target 下有没有产出 war 包,名字就是后面访问的 context path ls -l target/*.war

部署到 Tomcat 之前,先改数据库连接配置。这类项目通常把连接信息放在src/main/resources/jdbc.properties或db.properties里:

# 驱动类名要和 pom 里的 MySQL 驱动大版本对应 jdbc.driver=com.mysql.cj.jdbc.Driver # useUnicode 与 characterEncoding 一起写,只写一个不生效 jdbc.url=jdbc:mysql://127.0.0.1:3306/test_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=123456

URL 里四个参数各有分工:useUnicode和characterEncoding管中文不乱码,serverTimezone解决 8.x 驱动的时区异常,useSSL=false消掉启动时那一堆警告。jdbc.password改成你本机的密码,别照抄。

然后在 IDEA 里配 Tomcat:Run → Edit Configurations → + → Tomcat Server → Local,指定 Tomcat 安装目录;切到Deployment标签,点+添加Artifact,选xxx:war exploded,把Application context设成/demo。这一步没配对,最常见的表现是服务起来了但访问 404,因为 IDEA 默认可能把 context 设成/,或者 Artifact 压根没加进去。

3.3 冒烟链路:登录、列表、下单三跳

服务起来之后别急着开浏览器点。用 curl 走三个动作,能拿到比页面更准确的信息,也方便后面把这三个动作对应到 LoadRunner 的三个事务上。

# 1. 登录,把 Cookie 存下来,后面两步都要带着 curl -i -c cookie.txt \ -d "username=admin&password=123456" \ http://127.0.0.1:8080/demo/login # 2. 带 Cookie 访问订单列表,确认拿到的是数据页而不是跳回登录页 curl -s -b cookie.txt http://127.0.0.1:8080/demo/order/list | head -n 20 # 3. 提交一单,重点看状态码和返回体里的业务提示 curl -i -b cookie.txt \ -d "productId=1001&count=1" \ http://127.0.0.1:8080/demo/order/submit

三个动作分别验证三件事:登录接口能不能正确校验并下发会话;会话在跨请求时还认不认;写操作能不能穿过事务落到库。任何一跳断了,先修环境再写用例,别带着断链去设计测试。比如第 2 步返回的是登录页 HTML,说明 Filter 没放行或 Cookie 没带上,这时候去写“订单列表分页”的用例,跑出来必然全红,还会误判成业务缺陷。

冒烟通过后,把三个请求的 URL、方法、关键参数抄到一个临时的对照表里。后面 JUnit 用方法直接调,LoadRunner 用这三个 URL 建事务,两份工作共用一份事实,能省掉大量对不上的扯皮。

3.4 第一个 JUnit 用例:先证明测试链路通

自动化测试跑通的第一步,不是覆盖多少分支,而是证明“测试代码能连上被测代码”。先在pom.xml里补测试依赖:

<!-- JUnit 5,注意别和项目里已有的 JUnit 4 混用同一个 surefire 版本 --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.9.3</version> <scope>test</scope> </dependency>

然后写一个最小用例,直接打 Service 层,不启动 Tomcat,跑得快也不受端口影响:

class SmokeTest { @Test void 管理员账号应该能登录成功() { // 直接 new 业务类,前提是它不依赖 Servlet 容器里的对象 UserService service = new UserService(); LoginResult result = service.login("admin", "123456"); assertTrue(result.isSuccess(), "管理员登录失败,先检查 data.sql 是否导入"); assertNotNull(result.getUserId(), "登录成功但没有返回用户主键"); } }

这段代码的价值不在断言本身,而在于它验证了三件事:测试类能编译、Service 能实例化、数据库连接配置生效。如果卡在NullPointerException,八成是 Service 里用JdbcUtils拿连接时读不到配置文件——测试类不走 Tomcat,resources目录要靠 Maven 复制到target/test-classes,跑一次mvn test-compile再看那个目录里有没有jdbc.properties。

4. 测试执行两条线:JUnit 功能脚本与 LoadRunner 性能脚本拆解

功能测试和性能测试在这份资源里是两条独立线,但共用同一套业务入口。功能线用 JUnit 打 Service 层,追求覆盖率和可回归;性能线用 LoadRunner 打 HTTP 层,追求真实并发。两条线混着写是常见误区:用 JUnit 压并发测不出网络和容器瓶颈,用 LoadRunner 测单个分支又太重。

4.1 登录模块的等价类与边界值:用例表怎么变成可执行代码

登录是最适合入手的模块,输入维度少、分支清晰。资源里的用例表大致是这样组织的:

编号输入(用户名 / 密码)设计方法预期结果
C01admin / 123456有效等价类登录成功,跳转订单列表
C02admin / 12345无效等价类(密码长度不足)提示“用户名或密码错误”
C03空 / 123456无效等价类(必填为空)提示“用户名不能为空”
C0433 位超长用户名 / 123456边界值(长度上限 +1)提示“用户名长度超限”或截断处理
C05admin' or '1'='1 / 任意安全性用例登录失败,不能拼接出永真条件

表格里每一行都要能对应到代码里的一个判断分支。C04 这条边界值最容易出问题:如果数据库字段是varchar(32),超长输入在 MySQL 严格模式下会直接抛异常,页面上表现为 500 而不是友好提示,这时候到底算缺陷还是算设计如此,得看需求文档怎么写。我的判断标准是:能被普通用户触发的 500,就是缺陷,因为正常操作路径不该出现异常页。

C05 这条有的课程设计会省略,但它恰恰是能加分的地方。JavaWeb 项目里如果 DAO 层用字符串拼接 SQL,这条用例一跑就穿。检查办法很直接,打开 DAO 看有没有Statement加字符串拼接,有的话改成PreparedStatement占位符——注意这是改被测系统,如果课程设计只允许提交测试产物,那就把它写成一条高危缺陷记录,附上请求报文和返回内容。

4.2 JUnit 5 参数化:把用例表变成一次跑完的测试类

五条用例写成五个@Test方法也能跑,但改起来痛苦,且输出报告里看不出哪条对应哪个用例编号。用@ParameterizedTest把用例表直接搬进注解,编号进显示名,一眼能对上 Excel。

class LoginCaseTest { private static UserService service; @BeforeAll static void init() { // 整个类只初始化一次,避免每条用例都重建连接池 service = new UserService(); } @ParameterizedTest(name = "[{0}] {1} / {2} -> {4}") @CsvSource({ // 编号, 用户名, 密码, 是否成功, 说明 "C01, admin, 123456, true, 有效等价类", "C02, admin, 12345, false, 密码长度不足", "C03, admin, '', false, 密码为空", "C04, admin, 123456, true, 边界值下限外的对照组" }) void 登录用例(String caseId, String username, String password, boolean expectSuccess, String desc) { LoginResult result = service.login(username, password); // 断言里带上用例编号,失败时能直接定位到 Excel 行 assertEquals(expectSuccess, result.isSuccess(), caseId + " " + desc); if (!expectSuccess) { // 失败场景还要校验提示语,否则“提示不对”这类缺陷会被漏掉 assertTrue(result.getMessage().contains("用户名或密码"), caseId + " 提示语不符合预期:" + result.getMessage()); } } @AfterEach void clean() throws Exception { // 清掉本次用例可能写入的登录日志,保证重复执行结果一致 try (Connection c = JdbcUtils.getConnection(); PreparedStatement ps = c.prepareStatement( "DELETE FROM t_login_log WHERE create_time > ?")) { ps.setTimestamp(1, Timestamp.valueOf(LocalDateTime.now().minusMinutes(1))); ps.executeUpdate(); } } }

三个细节值得说清楚。@CsvSource里空字符串用''表示,如果直接留空,JUnit 会传null进去,而 Service 里的null和空串走的可能是不同分支,用例就测偏了。@AfterEach的清理不能省,登录日志这类表会随用例执行不断增长,第二次跑的时候如果业务里有“连续失败 5 次锁定账号”的逻辑,结果就会飘。断言里拼上caseId,是因为参数化测试的默认显示名信息量不够,出报告时看不出是第几行数据挂了。

数据库断言是另一半。只看LoginResult.isSuccess()只能证明返回值对,不能证明数据落地对。补一条独立的检查:

@Test void 登录成功后应该写入一条登录日志() throws Exception { service.login("admin", "123456"); try (Connection c = JdbcUtils.getConnection(); PreparedStatement ps = c.prepareStatement( "SELECT COUNT(*) FROM t_login_log WHERE username = ? AND status = 1")) { ps.setString(1, "admin"); try (ResultSet rs = ps.executeQuery()) { assertTrue(rs.next()); // 期望至少一条成功记录,多于此数说明前面用例的脏数据没清干净 assertTrue(rs.getInt(1) >= 1, "登录成功但未写入日志,检查 Service 的事务边界"); } } }

status = 1这个条件对应“成功登录”,失败登录应该写 0。这类字段如果业务里没定义,就得自己从schema.sql的注释里推断,或者干脆只断言记录数。我遇到过日志表和业务表不在同一个事务里,业务回滚了日志还留着,这种不一致在功能测试阶段很难发现,压测时才会暴露成数据对不上。

4.3 LoadRunner 脚本:录制只是起点,关联和事务才是正文

录出来的脚本直接用,十个有九个回放失败。原因是现代 Web 应用几乎都会下发会话标识或隐藏域令牌,录制时是固定值,回放时服务端一比对就拒绝。手工补关联是绕不过去的一步。

Action() { /* 集合点要放在事务外面,否则响应时间统计会把等待时间算进去 */ lr_rendezvous("login_rendezvous"); lr_start_transaction("login"); /* 先从登录页把隐藏域 token 抓出来,不做这步回放必然被拦 */ web_reg_save_param_ex( "ParamName=token", "LB=name=\"token\" value=\"", "RB=\"", SEARCH_FILTERS, "Scope=Body", LAST); web_url("login_page", "URL=http://127.0.0.1:8080/demo/login.jsp", "Resource=0", "RecContentType=text/html", LAST); /* 账号密码从参数文件取,同一账号并发会被业务踢下线 */ web_submit_data("do_login", "Action=http://127.0.0.1:8080/demo/login", "Method=POST", "EncType=application/x-www-form-urlencoded", ITEMDATA, "Name=username", "Value={username}", ENDITEM, "Name=password", "Value={pwd}", ENDITEM, "Name=token", "Value={token}", ENDITEM, LAST); /* 用 LR_AUTO 让它按 HTTP 状态码自动判定成功或失败 */ lr_end_transaction("login", LR_AUTO); /* 思考时间模拟真实用户停顿,不加会把压测做成纯打点 */ lr_think_time(2); return 0; }

web_reg_save_param_ex是注册型函数,必须写在它要抓取的那个请求之前,写在后面就抓不到。LB和RB是左右边界,从页面源码里复制,注意转义双引号。Scope=Body表示只在响应体里找,如果 token 藏在响应头里,要改成Scope=Headers或改用web_reg_save_param配合Search=Headers。

lr_start_transaction和lr_end_transaction圈出来的区间就是统计口径。登录这种动作,事务边界应该包住“打开登录页 + 提交表单”两步,因为用户感知的登录耗时就是这两步之和。有人把事务只包在提交那一步,出来的响应时间好看得离谱,答辩时一问就露馅。

参数化文件放在04_性能测试/scenario/下,通常是 dat 或 csv 格式,两列username,pwd,配几百条。参数化设置里,“选择下一行”用Sequential还是Random要看业务:如果账号之间没有关联数据,用Sequential加“循环时从头开始”,能保证每个虚拟用户拿到不同账号;用Random可能出现两个 VU 撞同一个账号,触发并发登录限制。

4.4 场景设计与指标判读:TPS 掉了先看哪里

脚本回放通过,只说明单用户能跑通。场景设计才是性能测试的主体,核心是三件事:加压方式、持续时间、监控指标。

指标含义课程设计里的合理参考
平均响应时间所有事务耗时的算术平均登录类操作控制在 1 秒内
90% 响应时间90% 的请求在此时间内完成与平均值差距不超过 3 倍
TPS每秒完成的事务数随并发上升,出现拐点即容量上限
错误率失败请求占比稳定期内低于 1%
吞吐量单位时间传输的数据量与 TPS 趋势应基本一致

加压方式建议用阶梯:10、20、50、100 个虚拟用户,每档跑 5 分钟,中间留 1 分钟降压。一次性拉满 100 个用户,得到的只是一条平线,看不出系统在哪个并发点开始劣化。阶梯加压的曲线会呈现三段:线性增长段说明资源还有余量;拐点附近 TPS 增速放缓,响应时间开始抬升;再往上 TPS 掉头向下,说明瓶颈已经出现,这时候要看 Tomcat 线程池、数据库连接池和慢查询三个地方。

判读时有个血泪经验:先看错误率再看响应时间。如果错误率突然从 0 跳到 20%,响应时间的平均值会被快速失败拉低,看起来“变快了”,实际上是大量请求被拒绝。我一般在场景里加一个固定的检查点,每档结束后确认错误率曲线是平的,再去看 TPS。

5. 避坑排查:五个把课程设计卡住的现场

下面五条是我在这类项目上反复见到的现场,每条按现象、原因、解决写。有的属于环境问题,有的属于用例设计问题,混在一起看更容易对上号。

5.1 页面返回 500,日志里是一串问号

现象:登录页提交后跳到错误页,Tomcat 控制台或日志里出现一串???,看不出具体异常。

原因:MySQL 连接串没带字符集参数,或者建库时用了latin1。数据库存中文、Java 端按 UTF-8 解,中间任何一环不一致都会变成乱码;乱码再拿去比对密码或用户名,自然查不到记录。

解决:三处一起改。建库语句加DEFAULT CHARACTER SET utf8mb4;连接串加useUnicode=true&characterEncoding=utf8;JSP 页面顶部补<%@ page contentType="text/html;charset=UTF-8" %>。改完必须删库重建,光改连接串不会修复已经存成乱码的数据。

5.2 单条用例跑通,全量跑就挂

现象:单独点某条 JUnit 用例能过,整个类一起跑就有一两条失败,重新跑一次,失败的又是另外两条。

原因:用例之间有状态污染。前一条用例插入的数据没清,后一条按主键查就查出多条;或者依赖执行顺序,C01必须排在C02前面才对。

解决:每条用例自己准备数据、自己清理,用@BeforeEach灌入、@AfterEach清除。清理语句按时间戳或固定主键删,不要用DELETE FROM t_order全表清,那样会连带删掉其他用例的数据。另外把测试账号的主键固定下来,别用自增 ID,避免顺序依赖。

5.3 LoadRunner 回放 403,录制时明明是对的

现象:脚本原样回放,登录请求返回 403 或跳回登录页,而手工点页面一切正常。

原因:缺少关联。服务端在登录页下发了会话标识或隐藏域令牌,回放时用的是录制那一刻的旧值,服务端校验失败。第二个可能是请求头里的Referer或User-Agent被服务端做了校验。

解决:先在浏览器里抓两次登录页的响应,比对每次的隐藏域值是否不同。不同就加web_reg_save_param_ex提取,注意函数位置要在请求之前,左右边界从源码里一字不差地复制。如果令牌是通过Set-Cookie下发的,LoadRunner 默认会处理 Cookie,但仍要在运行设置里确认“启用 Cookie”,并且不要把web_cleanup_cookies写在事务里。

5.4 压测跑起来 TPS 很低,错误率却为 0

现象:100 个虚拟用户加压,TPS 只有个位数,但错误率一直是 0,响应时间也不高。

原因:事务边界或者思考时间设置有问题。常见的是lr_think_time设了 5 秒以上,虚拟用户大部分时间在等待,真正发压的时间很少;另一种是事务没包住真实操作,只统计了一个静态资源请求,TPS 看起来“稳定”其实毫无意义。

解决:先用 1 个虚拟用户跑一遍,看脚本迭代一次的真实耗时,再乘并发数估算理论 TPS。实测值远低于估算,就检查思考时间、事务边界和集合点位置。集合点放在事务外,事务只包业务动作,思考时间压到 1 到 3 秒,这组配置跑出来的数据才对得上。

5.5 Tomcat 起来了,访问就是 404

现象:IDEA 控制台显示 Tomcat 启动成功,浏览器打开http://localhost:8080/却是 404,或者是一个空目录列表。

原因:Artifact 没部署、Application context配错、war 包没打进去。IDEA 的 Tomcat 配置里,Deployment标签页没加 Artifact,服务能起但没有任何应用;另一个常见原因是 context 设成了/,而项目里所有跳转都写死了/demo/login,对不上。

解决:到Deployment确认有一条xxx:war exploded,把Application context改成/demo,与pom.xml里的finalName保持一致。改完执行一次mvn clean package,让 IDEA 重新构建 Artifact,再重启。如果还是 404,去target目录看webapp下的 JSP 有没有被复制进去,Maven 的资源过滤配置有时会把页面漏掉。

6. 二次开发:把这套东西改成能交给自己的版本

这套资源最值钱的地方不是它自带的用例,而是它给了一套可替换的骨架。你要交的课程设计不可能和它一模一样,改造的第一步是换掉被测系统里最容易替换的部分:把controller里的业务方法换成自己的模块,比如把订单查询改成图书借阅查询,只改 URL 映射和 SQL,用例表里的输入输出跟着调,JUnit参数化注解里的@CsvSource数据块换掉就行。

第二步是补全测试资产。资源里的用例表通常只覆盖登录、注册、查询这几个基础模块,你可以按“等价类 + 边界值 + 异常路径”的套路给新模块补一张表。补的时候别贪多,一个模块 8 到 12 条就够,重点保证每条都有明确的输入、预期和对应的方法分支。

改造点改哪里怎么验证
换业务模块controller的映射路径、service方法名冒烟三个动作能走通
换数据库schema.sql、data.sql、jdbc.properties核对表行数,跑一条查询用例
换测试数据@CsvSource数据块、参数化 dat 文件单条跑通后全量跑两遍,结果一致
调压测规模场景里的虚拟用户数、持续时间阶梯加压看拐点是否可复现
换报告模板03_测试资产/报告下的文档结构指标数值与场景运行结果对得上

第三个技巧是收敛压测变量。性能报告最怕数据不可复现,所以每次只改一个变量:先固定 50 并发跑三次,确认 TPS 波动在 10% 以内,再单独改数据库连接池大小,看拐点往哪边挪。连接池参数通常写在context.xml或db.properties里,maxActive从 20 调到 50,如果 TPS 没变化,说明瓶颈不在连接池,去看慢查询和 Tomcat 线程数。

验证改造是否成功,我用的办法很笨但可靠:把03_测试资产里的用例表打印出来,对着代码一条一条走,每条用例能在controller里找到入口、在service里找到分支、在schema.sql里找到对应字段,三处都找到才算这条用例真的落地了。找不到的,要么是用例写虚了,要么是代码里根本没有这个逻辑,两种情况都得处理。

从那以后我每次接手这类课程设计包,都强制自己先干一件事:把用例表的编号和代码分支对一遍,对不上的先改文档再动手跑脚本。看起来慢了半天,实际上省掉了后面反复排查假失败的时间。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进