news 2026/10/31 3:16:40

JavaWeb软件测试课程设计:JUnit功能测试与LoadRunner性能压测

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaWeb软件测试课程设计:JUnit功能测试与LoadRunner性能压测

简介:《软件测试课程设计》配套资料包围绕JavaWeb网上书店前台系统展开,适合计算机相关专业学生完成测试课程设计、复盘测试方法或准备答辩时参考。压缩包共904个文件、约45.97MB,包含jsp页面、java源码、class编译文件、xml与properties配置、jar依赖及gif、jpg、png页面素材,另有js、css等前端文件,较完整地保留了基于IDEA开发、MySQL支撑的项目结构与测试材料。已有2351人学习,在同类课程设计资料中具有一定参考度。内容覆盖白盒测试的逻辑覆盖与基本路径覆盖用例设计,黑盒测试的等价类、边界值分析,核心功能的JUnit单元测试,以及手工或自动化功能测试;性能部分借助LoadRunner开展稳定性与压力测试,通过逐步加压观察性能变化并确定最大服务级别。读者可据此梳理从用例设计、单元测试到性能验证的完整流程,形成可复用的测试思路与课程设计报告素材。

1. 从一份压缩包说起:软件测试课程设计交付的到底是什么

软件测试课程设计.zip这类资源最容易踩的坑,是被测系统和测试产物两张皮:系统是从别处搬来的 JavaWeb 登录页,测试用例是照着模板编的,LoadRunner 脚本录完就再没回放过。这份包的价值在于它把被测系统和对准它的测试材料放在了一起——MySQL 建表脚本、功能测试用例表、JUnit 单元测试、性能测试脚本、缺陷记录和测试报告,全部指向同一个 JavaWeb 学生信息管理模块。

它适合两类人:一是要交课程设计、但卡在「系统能跑、测试不知道从哪下手」的同学;二是写了几年 JavaWeb、功能能交付却从没系统做过等价类划分和并发压测的开发。前者能照着复现整条流程,后者能看清一个业务方法为了「可测」需要被改成什么样。下面按「先跑起来、再拆代码、然后测功能、最后压性能」的顺序走一遍。

2. 工程结构与启动链路:一份压缩包怎么变成能跑的 JavaWeb 系统

2.1 技术选型:为什么用 Servlet + JDBC 而不是上来就 SSM

这套组合是 IDEA + JavaWeb(Servlet/JSP)+ MySQL + JDBC + JUnit,性能测试部分用 LoadRunner。很多人看到「课程设计」就默认该上 Spring Boot,但对测试练习来说,Servlet + JDBC 反而更合适:请求进出的边界清晰,一个doPost里能直接看到参数解析、业务校验、SQL 执行三段,写测试用例时能精确对应到某个分支。上了框架之后,大量逻辑被注解和拦截器吞掉,反而不好验证「这个校验到底拦没拦住」。

JDK 用 8 或 11 都行,我一般选 8,因为课程设计常见的 Tomcat 版本对 8 的支持最省事。数据库驱动注意用com.mysql.cj.jdbc.Driver,老的com.mysql.jdbc.Driver在新版驱动里已经废弃,直接写会报Loading class ... deprecated,虽然不一定崩,但第一次跑就冒红字容易让人心态崩。

工程用 Maven 管依赖,pom.xml里核心就四块:javax.servlet-api(scope 设为 provided,因为运行时由 Tomcat 提供)、mysql-connector-java、junit-jupiter、mockito-core。provided 这条经常被漏,漏了就会出现「本地跑得好好的,部署到 Tomcat 报类冲突」的经典事故。

2.2 目录结构:doc、src、sql、lr_script 四块分别装什么

解压后大致是这么个骨架,我按实际项目里最常见的组织方式画出来:

software-testing-cd/ ├── doc/ │ ├── 测试计划.docx │ ├── 测试用例.xlsx │ ├── 缺陷记录.xlsx │ └── 测试报告.docx ├── sql/ │ └── schema.sql ├── src/ │ ├── main/ │ │ ├── java/com/example/sms/ │ │ │ ├── controller/ StudentServlet.java LoginServlet.java │ │ │ ├── service/ StudentService.java │ │ │ ├── dao/ StudentDao.java │ │ │ ├── entity/ Student.java │ │ │ └── util/ DbUtil.java │ │ ├── resources/ │ │ │ └── jdbc.properties │ │ └── webapp/ │ │ ├── WEB-INF/web.xml │ │ └── jsp/ list.jsp login.jsp edit.jsp │ └── test/java/com/example/sms/ │ └── StudentServiceTest.java ├── lr_script/ │ └── login_query/ └── pom.xml

四块的分工要记牢:doc是交付物,src是被测系统加单元测试,sql是可重复执行的建库脚本,lr_script是性能脚本目录。二次开发时最常改的是src和sql,doc里的用例表结构基本不用动,只换内容。

jdbc.properties单独抽出来是刻意的:测试环境和开发环境要切库的时候,只改这一个文件,不用动代码。测试专用库我一般命名为sms_test,跟开发库sms_dev隔开,避免跑单元测试把演示数据洗掉。

2.3 建库建表与测试数据:一份能重复执行的 schema.sql

schema.sql要写成可重复执行的,开头带DROP TABLE IF EXISTS,否则第二次导入就报主键冲突,这种低级错误在答辩现场特别致命。

-- 测试专用库,和开发库隔离 CREATE DATABASE IF NOT EXISTS sms_test DEFAULT CHARACTER SET utf8mb4; USE sms_test; DROP TABLE IF EXISTS student; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, sno CHAR(8) NOT NULL UNIQUE, -- 学号,8 位数字,唯一约束是测试重点 name VARCHAR(20) NOT NULL, gender TINYINT NOT NULL DEFAULT 1,-- 1 男 2 女 age TINYINT NOT NULL, phone CHAR(11) NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; DROP TABLE IF EXISTS sys_user; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(20) NOT NULL UNIQUE, password CHAR(32) NOT NULL -- 存 MD5,测 SQL 注入时要注意这一层 ); -- 基础数据:够分页和模糊查询用即可 INSERT INTO sys_user(username, password) VALUES ('admin', MD5('admin123')); INSERT INTO student(sno, name, gender, age, phone) VALUES ('20240001', '张同学', 1, 20, '13800000001'), ('20240002', '李同学', 2, 21, '13800000002'), ('20240003', '王同学', 1, 19, '13800000003');

这里有几个为测试服务的刻意设计。sno加了唯一约束,是为了测「重复学号」时有两道防线——代码里先查一次给友好提示,数据库再兜一次;测的时候要分别验证这两层。age用TINYINT而不是字符串,边界值用例直接落在 15 和 60 上。name限制 20 字符,超长截断和报错是两条不同的分支。

测试数据只造 3 条是故意的:分页每页 2 条,正好测试「第 1 页满、第 2 页半满、第 3 页空」三种状态,数据造太多反而看不出边界。

2.4 核心链路代码:一个查询请求从 Servlet 走到 ResultSet

先把连接工具类写扎实,后面所有 DAO 都靠它。这里用静态块加载配置,启动时就把驱动加载好,避免每次取连接都反射一遍。

public class DbUtil { private static final Properties P = new Properties(); static { // try-with-resources 保证流关闭,配置文件加载失败直接抛错,别吞异常 try (InputStream in = DbUtil.class.getClassLoader() .getResourceAsStream("jdbc.properties")) { if (in == null) { throw new IllegalStateException("jdbc.properties 未找到"); } P.load(in); Class.forName(P.getProperty("jdbc.driver")); } catch (Exception e) { throw new ExceptionInInitializerError(e); // 类初始化阶段失败,让问题尽早暴露 } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection( P.getProperty("jdbc.url"), P.getProperty("jdbc.username"), P.getProperty("jdbc.password")); } }

对应的jdbc.properties,注意 URL 里三个参数一个都不能少:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/sms_test?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=123456

characterEncoding=utf8管中文,serverTimezone管时间字段差 8 小时,useSSL=false是本地连接免掉证书告警。少一个就可能出现乱码或者时间对不上,这类问题在测试记录里被归成「系统 Bug」其实很冤。

DAO 层的关键方法是分页加模糊查询,注意LIMIT的两个参数也用占位符:

public class StudentDao { public List<Student> findByPage(String keyword, int pageNo, int pageSize) throws SQLException { // 关键字为空时条件恒真,一条 SQL 覆盖「全部列表」和「模糊搜索」两种场景 String sql = "SELECT id, sno, name, gender, age, phone FROM student " + "WHERE (? = '' OR name LIKE CONCAT('%', ?, '%') OR sno LIKE CONCAT('%', ?, '%')) " + "ORDER BY id LIMIT ?, ?"; List<Student> list = new ArrayList<>(); try (Connection conn = DbUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, keyword); ps.setString(2, keyword); ps.setString(3, keyword); ps.setInt(4, (pageNo - 1) * pageSize); // 页码从 1 开始,偏移量要减 1 ps.setInt(5, pageSize); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { list.add(mapRow(rs)); } } } return list; } public boolean existsBySno(String sno) throws SQLException { String sql = "SELECT 1 FROM student WHERE sno = ? LIMIT 1"; try (Connection conn = DbUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, sno); try (ResultSet rs = ps.executeQuery()) { return rs.next(); } } } }

existsBySno单独抽出来而不是在 Service 里查列表再判断,是为了让「学号唯一性校验」这一条能被 mock 精准控制——单元测试里让它返回 true 或 false,就能覆盖两条分支,不用真连库。

Servlet 层只做三件事:取参、调 Service、转发视图。

@WebServlet("/student") public class StudentServlet extends HttpServlet { private StudentService service; @Override public void init() { service = new StudentService(new StudentDao()); // 构造注入,测试时可替换实现 } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String keyword = req.getParameter("keyword"); if (keyword == null) keyword = ""; // 空指针是列表页最常见的 500 来源 int pageNo = parseInt(req.getParameter("pageNo"), 1); int pageSize = 2; try { List<Student> list = service.page(keyword.trim(), pageNo, pageSize); req.setAttribute("list", list); req.getRequestDispatcher("/jsp/list.jsp").forward(req, resp); } catch (SQLException e) { req.setAttribute("msg", "查询失败:" + e.getMessage()); req.getRequestDispatcher("/jsp/error.jsp").forward(req, resp); } } }

parseInt那里要有个兜底方法,参数传abc或负数时回落到第 1 页。这种「脏参数」正是功能测试里最容易漏、又最容易在演示时被点出来的地方。

2.5 为可测性做的三处改造

原始版本的代码通常长这样:new StudentDao()写在方法体里,校验逻辑和 SQL 混在一起,异常直接e.printStackTrace()。这种代码能跑,但没法测。要动三刀。

第一刀,依赖从「方法内 new」改成「构造函数传入」。Service 持有 DAO 接口引用,单元测试时塞 Mockito 生成的 mock 对象进去,就能在不连数据库的前提下验证「学号重复是否拦截」。

第二刀,把校验逻辑从 Servlet 挪进 Service,并抛出统一的BizException。Servlet 里只剩取参和转发,校验分支可以单独断言消息文案。文案本身也是测试对象,比如「年龄超出范围」和「年龄必须是数字」必须是两条不同提示,否则前端提示就没了意义。

第三刀,DAO 方法一律声明throws SQLException,不吞不包。吞掉异常最坑的地方是测试时永远拿不到失败原因,e.printStackTrace()打在控制台,用例只能判「页面白了」,判不出为什么白。

改造后的 Service 大概这样:

public class StudentService { private final StudentDao dao; public StudentService(StudentDao dao) { this.dao = dao; } public void add(Student s) throws SQLException { if (s.getSno() == null || !s.getSno().matches("\\d{8}")) { throw new BizException("学号必须是8位数字"); } if (s.getAge() < 15 || s.getAge() > 60) { throw new BizException("年龄需在15到60之间"); } if (dao.existsBySno(s.getSno())) { throw new BizException("学号已存在"); } dao.insert(s); } }

三处校验对应三类用例:格式、边界、唯一性。每类都能用一次 mock 调用验完,不需要启动 Tomcat,回归成本从「重启容器两分钟」降到「跑测试两秒钟」。这一步做完,后面写用例才有意义。

3. 功能测试落地:从测试点拆解到 JUnit 断言

3.1 需求到测试点:把功能清单翻译成可执行项

被测系统的功能清单不长:登录、学生列表分页、关键字搜索、新增、编辑、删除。直接对着功能写「测试登录成功」这种用例,写十条也覆盖不了分支。我的做法是先按「输入域 + 处理逻辑 + 输出」三列拆,把每个功能拆成若干可判定的点。

以新增学生为例,拆出来是这些点:学号格式(8 位纯数字 / 7 位 / 9 位 / 含字母 / 含空格)、学号唯一性(库中已存在 / 不存在 / 大小写差异)、姓名(空 / 1 字 / 20 字 / 21 字)、年龄(14 / 15 / 60 / 61 / 非数字)、手机号(11 位 / 10 位 / 含字母)、以及并发提交同一学号。拆完再看,光新增一个功能就有二十多个测试点,用例表填起来才不空。

这一步的产出直接决定后面两章的质量。测试点拆得粗,等价类就没法划;等价类划不对,JUnit 里写的断言就是「跑通即通过」,等于没测。我在项目里见过太多用例表只有「正常新增」「异常新增」两行,一问边界值多少,答不上来。

还有一类容易漏的点:页面上看不见但系统里存在的约束。比如数据库sno有唯一索引,那么「绕过前端直接 POST 重复学号」必须能返回友好提示而不是 500。这类用例要借助接口工具或 LoadRunner 脚本发原始请求,浏览器点不出来。

3.2 等价类与边界值:登录、新增学生两张用例表

登录功能的有效等价类只有一个:用户名存在且密码匹配。无效等价类就多了,值得列成表:

编号输入(用户名 / 密码)等价类归属预期结果
L-01admin / admin123有效跳转列表页,Session 中有 user
L-02admin / admin124密码错误提示「用户名或密码错误」
L-03空 / admin123用户名为空前端拦截,提示必填
L-04admin / 空密码为空前端拦截,提示必填
L-05notexist / admin123用户不存在与密码错误提示一致,不暴露账号是否存在
L-06admin' -- / 任意注入尝试登录失败,不产生 SQL 异常

L-05 和 L-02 提示文案必须一致,这是安全设计上的要求,也正好是一条可断言的用例。L-06 用来验证预编译是否真的生效——如果代码里是字符串拼 SQL,这条会直接登进去,属于高危缺陷。

新增学生的边界值集中在年龄和学号长度上:

编号字段取值类型预期
A-01age15边界内最小值成功
A-02age14边界外提示「年龄需在15到60之间」
A-03age60边界内最大值成功
A-04age61边界外同上
A-05sno20240001(已存在)唯一性冲突提示「学号已存在」
A-06sno2024000长度不足提示「学号必须是8位数字」

写这张表的时候有个习惯我一直保留:每个边界值上下各取一个点,而不是只取边界本身。只测 15 和 60,测不出「大于等于」被写成「大于」的问题,因为 60 在两种写法下都能过;加上 14 和 61,才能把比较符钉死。

3.3 判定表与场景法:多条件组合怎么收敛

搜索功能的条件组合是个典型:关键字为空/非空、页码合法/非法、排序字段合法/非法,三组条件笛卡尔积是 8 种。全测没必要,用判定表收敛到 4 条有效规则就够。

规则关键字页码排序字段动作
R1空1默认返回全部,按 id 升序
R2非空1默认模糊匹配 name 或 sno
R3空最后一页+1默认返回空列表,不报错
R4任意非法(abc / -1 / 0)任意回落到第 1 页

R3 是很多人第一次跑就翻车的地方:页码越界时 SQL 的LIMIT偏移量超出总数,返回空集,但 JSP 里如果做了list.get(0)就直接IndexOutOfBoundsException。这条用例的价值不在「功能对不对」,而在「异常处理有没有做」。

场景法用在跨功能的流程上,比如「登录 → 搜索 → 编辑 → 保存 → 回到列表验证数据已变」。这条链路串起了 Session 保持、参数回显、更新语句三块,任何一环断了都能被这条用例抓到。手工执行一遍大概两分钟,但它是回归测试里最值得保留的那条。

3.4 JUnit 落地:Service 层单测怎么写才有价值

手工用例写完后,把能自动化的部分落到 JUnit。优先自动化 Service 层,因为校验逻辑都在那儿,且不依赖容器。用 JUnit 5 加 Mockito,测试类放在src/test/java下对应包路径。

class StudentServiceTest { private StudentDao dao; private StudentService service; @BeforeEach void setUp() { dao = Mockito.mock(StudentDao.class); // 不连库,行为完全由用例控制 service = new StudentService(dao); } @Test @DisplayName("A-05 学号重复时应拦截且不落库") void add_duplicateSno_shouldThrow() throws Exception { Student s = new Student("20240001", "张同学", 1, 20, "13800000001"); Mockito.when(dao.existsBySno("20240001")).thenReturn(true); BizException ex = assertThrows(BizException.class, () -> service.add(s)); assertEquals("学号已存在", ex.getMessage()); Mockito.verify(dao, Mockito.never()).insert(Mockito.any()); // 关键断言:没写库 } @ParameterizedTest(name = "age={0} 期望={1}") @CsvSource({ "14, 年龄需在15到60之间", "15, OK", "60, OK", "61, 年龄需在15到60之间" }) void add_ageBoundary(int age, String expected) throws Exception { Student s = new Student("20240009", "李同学", 2, age, "13800000009"); Mockito.when(dao.existsBySno("20240009")).thenReturn(false); if ("OK".equals(expected)) { service.add(s); Mockito.verify(dao).insert(s); // 正向用例要确认真的调了 insert } else { BizException ex = assertThrows(BizException.class, () -> service.add(s)); assertEquals(expected, ex.getMessage()); } } }

两个细节值得说。一是verify(dao, never()).insert(any())这句不能省,很多人只断言抛了异常就收工,结果代码里「先 insert 再校验」这种顺序错误测不出来。二是正向用例要断言insert被调用一次,而不是只看「没抛异常」,因为 Service 方法完全可能什么都不做也不抛错。

DAO 层的测试另说,它需要真连数据库。常见做法是建一个sms_test库,@BeforeEach里开事务、@AfterEach里回滚,保证用例之间不互相污染。如果不想装 MySQL,也可以换内存库跑,但要接受 SQL 方言差异——LIMIT ?, ?在多数内存库里也支持,CONCAT就不一定了,这一层心里要有数。

3.5 缺陷记录与回归验证

缺陷表不用复杂,五列够用:编号、关联用例、现象、严重程度、状态。关键是「关联用例」这一列必须填,否则改完代码不知道回归哪几条。

举个真实会出现的例子:A-05 在手工测试时通过,但自动化用例挂了。查下来是 Service 里先insert再existsBySno,手工测的时候因为数据库唯一约束兜底还是报了错,页面上看不出差别;单测因为 mock 掉了 DAO,insert被调用就被抓出来了。这类「表现一致、实现错误」的问题,只有断言到调用行为才看得见。

回归的触发条件写清楚比写全更重要:改 Service 校验逻辑 → 回归 3.4 全部单测;改 SQL → 回归 3.3 的判定表四条;改前端 → 回归 3.4 的场景链路。别每次改一行都全量回归,跑不起就没人跑了。

4. 性能测试:LoadRunner 脚本录制、参数化与场景设计

4.1 协议选择与录制:从浏览器操作到脚本骨架

被测系统是纯 HTTP 的 JavaWeb 应用,协议选 Web (HTTP/HTML) 就行,别选 Web Services,那个是给 SOAP 用的。录制前先把浏览器代理指向 LoadRunner 的录制端口,清掉缓存,然后完整走一遍「登录 → 搜索 → 翻页」三条操作,录完能得到一串web_submit_data和web_url。

录制出来的脚本不能直接用,三个地方必须手改:删掉静态资源请求(jpg、css、js),它们对业务 TPS 没意义只会干扰指标;删掉录制工具自动加的web_add_cookie,改由关联统一处理;把每个业务动作包进事务,否则报表里只有总响应时间,看不出登录慢还是查询慢。

4.2 参数化:账号池、数据文件与迭代策略

用同一个账号压测等于自欺欺人,服务端 Session 会互相顶掉,测出来的错误率没有参考意义。准备一个user.dat,第一行写参数名,后面每行一组:

username,password admin,admin123 test01,test123 test02,test123 test03,test123

脚本里把硬编码的admin换成{username}、{password},然后在参数列表里把 File 指向这个文件,选择「Unique + Once」或者「Sequential + Each iteration」。选 Unique 的好处是并发用户各自拿到不同账号,不会出现会话互踢;缺点是账号数必须大于等于最大并发数,否则跑到后面会取空。

账号数量这件事要提前算:计划压 50 并发,账号至少准备 50 个,最好 60 个留余量。我见过压测跑到一半错误率突然飙到 30%,排查半天发现是账号池只有 20 个,后面 30 个虚拟用户全拿着空用户名在登录。

搜索关键字同样要参数化,否则所有人都查同一个词,数据库缓存一命中,TPS 好看得不像真的。关键字文件里混一些命中率低的词,比如只匹配一条记录的生僻字,能更真实地反映索引效率。

4.3 关联:JSESSIONID 与登录态怎么保住

这是 LoadRunner 最容易翻车的一环。登录成功后服务端通过Set-Cookie下发JSESSIONID,后续请求必须带上,否则每个请求都被当成未登录,直接 302 回登录页,压出来的全是重定向。

Action() { lr_start_transaction("login"); // 从响应头里抓 JSESSIONID,LB/RB 是左右边界,Scope 必须指定 Headers web_reg_save_param_ex( "ParamName=JSESSIONID", "LB=Set-Cookie: JSESSIONID=", "RB=;", SEARCH_FILTERS, "Scope=Headers", LAST); web_submit_data("login", "Action=http://127.0.0.1:8080/sms/login", "Method=POST", "EncType=application/x-www-form-urlencoded", ITEMDATA, "Name=username", "Value={username}", ENDITEM, "Name=password", "Value={password}", ENDITEM, LAST); lr_end_transaction("login", LR_AUTO); lr_start_transaction("search"); web_custom_request("search", "URL=http://127.0.0.1:8080/sms/student?keyword={keyword}&pageNo=1", "Method=GET", "Resource=0", LAST); lr_end_transaction("search", LR_AUTO); return 0; }

web_reg_save_param_ex必须写在产生该参数的请求之前,这是注册函数不是普通函数,位置错了就抓不到。Scope=Headers不能省,因为响应体里也可能出现同名内容,抓错了后面全部请求都带一个无效会话。

如果登录用的是重定向而不是直接返回页面,还要在脚本里把Redirect相关选项打开,让 LoadRunner 自动跟随 302。否则事务结束在 302 那一步,响应时间看着只有几十毫秒,实际上业务根本没跑。

4.4 事务、集合点与场景设计

事务粒度按业务动作拆,登录一个、搜索一个、翻页一个,别把三个包成一个大事务,那样报表里只有一个平均值,定位不到瓶颈。事务名和功能测试用例编号对得上更好,出问题能直接翻回用例表。

集合点lr_rendezvous("search_point")加在搜索请求之前,作用是让所有虚拟用户到齐了再同时发。不加的话用户是错峰跑的,压不出真正的同时并发峰值。但集合点只适合做峰值验证,长稳压测时要去掉,否则虚拟用户之间互相等待,TPS 会被拉低。

场景设计分两段:基准测试和负载测试。基准测试用 1 个用户跑 5 分钟,拿到单用户的响应时间基线,这个数在后面的报表里是分母。负载测试用阶梯加压,每 30 秒加 10 个用户,加到 50 后持续跑 10 分钟,观察 TPS 曲线的拐点。拐点出现的位置就是系统的实际承载上限,这个结论比「支持 50 并发」这种孤立数字有用得多。

思考时间设为 1 到 3 秒随机,模拟真实用户的操作间隔。设成 0 的话等于每个用户都在满负荷发请求,压出来的数字毫无参考价值,答辩时也容易被问住。

4.5 结果指标:TPS、响应时间与错误率怎么读

分析页面上重点看四个数。平均事务响应时间看趋势不看均值,均值会被少量超长请求拉歪;90% 响应时间更稳,它表示 90% 的请求在这个时间之内完成,通常拿来和需求指标对齐;TPS 看曲线是否平稳,锯齿状说明有资源争抢;错误率超过 1% 就要停下来查原因,别硬压。

常见的组合判读是这样的:TPS 上升到某个点后不再涨,响应时间同时开始线性上升,错误率为 0——这是典型的容量拐点,说明线程池或连接池打满了,服务端在排队。如果 TPS 掉、错误率涨,那多半是连接池耗尽后抛异常,去查数据库的max_connections和连接池上限。

还有一个容易被忽略的点:压测机和被测机别放同一台。本机压本机,CPU 互相抢,TPS 曲线完全没有意义。至少分两台机器,压测机配置别低于被测机,否则先撑不住的是压测工具自己。

5. 避坑与排查:跑不起来、压不出数据的时候看这几处

现象:IDEA 里启动 Tomcat 报ClassNotFoundException: com.mysql.cj.jdbc.Driver,但pom.xml里明明有依赖。原因是依赖 scope 写成了provided,或者 Maven 依赖没同步到 artifacts 里。解决方式是把 MySQL 驱动的 scope 改成默认的 compile,再进 Project Structure 的 Artifacts 面板确认mysql-connector在WEB-INF/lib里;实在不行把 jar 手动丢进webapp/WEB-INF/lib兜底。

现象:中文姓名查出来是问号,编辑保存后变乱码。原因通常是三层里有一层没设 UTF-8:数据库建库时用了 latin1、JDBC URL 少了characterEncoding=utf8、或者 Servlet 里没写resp.setContentType("text/html;charset=UTF-8")。排查顺序从库往上走,先SHOW CREATE DATABASE sms_test看字符集,再看 URL,最后看web.xml里的过滤器。三处都对了还乱码,检查 Tomcat 的server.xml里 Connector 有没有URIEncoding="UTF-8"。

现象:LoadRunner 录制完回放,所有请求返回 302,事务全部「成功」但业务没跑。原因是没做关联,每个请求都被重定向到登录页,而 LoadRunner 默认把 302 也算成功。解决分两步:先用web_reg_save_param_ex抓JSESSIONID并确认抓到了值(在run-time viewer里能看到参数值才算成功),再在运行时设置里把「HTTP 重定向」打开,让脚本跟随跳转。抓不到值的话检查边界字符串是否和实际响应头完全一致,冒号后面有没有空格都可能影响。

现象:JUnit 测试单独跑能过,一起跑就互相干扰,第二条用例报「学号已存在」。原因是 DAO 层测试没做数据隔离,前一条用例插进去的数据留在了库里。解决方式是在测试类上加@Transactional让每条用例自动回滚,或者在@BeforeEach里清表重灌种子数据。还有一种更隐蔽的情况:Service 层测试里 mock 对象在@BeforeEach中重建了,但静态字段缓存了旧状态,这种要看被测类里有没有static可变成员。

现象:压测 TPS 只有个位数,看服务端日志一切正常。先排除压测侧问题:虚拟用户数是不是只设了 1 个、思考时间是不是设成了 10 秒、日志级别是不是开到了 DEBUG(每条请求打一堆日志,磁盘 IO 先成瓶颈)。再排除数据库侧:连接池上限是不是只有 5 个,50 个并发抢 5 个连接,剩下的全在等。这两处都是配置问题,代码本身没毛病,但表现上特别像「系统性能差」。

6. 二次开发:把这套测试资产换成你自己的项目

最省事的复用方式不是重新写一遍,而是把「骨架」和「内容」拆开。骨架是目录结构、用例表字段、JUnit 的 mock 模式、LoadRunner 的关联和事务框架;内容是具体的表名、字段、校验规则和业务流。换项目时只动内容,骨架照抄。

具体操作分三步。第一步换被测系统:把student表和对应的 Service 换成你自己的业务实体,pom.xml依赖不动,DbUtil不动,Servlet 的取参转发模式不动。第二步换用例:3.2 那两张等价类表按新字段重填,边界值这一列必须重算——比如你的字段是金额,边界就不是 15 和 60,而是 0、0.01、最大值和最大值加一分。第三步换脚本:LoadRunner 里改Action的 URL 和ITEMDATA的字段名,参数化的 dat 文件按新登录接口调整,关联规则一般不用动,因为 Session 机制都差不多。

几个能明显提升含金量的改造方向:

方向做法收益
接口自动化用 RestAssured 或 HttpClient 把 3.2 的用例表脚本化,接进 JUnit用例从文档变成可回归资产
数据隔离DAO 测试统一@Transactional回滚 + 独立测试库用例可并行,不再互相污染
持续集成提交时自动跑单测,失败阻断合并校验逻辑改动立刻暴露
压测数据化把 LoadRunner 结果导出,和上一版对比生成趋势图性能回归有据可依

有一处我踩过不止一次:把被测系统的库直接拿来做压测库,压完发现演示数据被改得面目全非,学号重复、年龄越界的数据全留在里面。从那以后我每次开压之前都强制走一遍「建库脚本重建 + 种子数据导入 + 校验条数」这三步,压测和功能用两个独立的库,谁也不碰谁。

改造过程中还有个小技巧:先让旧用例在新系统上跑一遍,看哪些挂了。挂掉的用例要么是新系统真的缺功能,要么是用例本身写死了旧字段,两种情况都能帮你快速摸清新项目的边界。这比从头读代码快得多。

希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 3:16:30

SigmaWin+ V7.02 64位USB驱动安装与连接排查指南

简介&#xff1a;面向使用Yaskawa Σ系列伺服驱动器、变频器等工业设备的调试工程师与维护人员&#xff0c;这份SigmaWin V7.02配套的64位Windows USB驱动安装包&#xff0c;主要用于建立PC与设备之间的稳定通信&#xff0c;支撑参数设定、运行监控与现场故障排查。压缩包共91个…

作者头像 李华
网站建设 2026/10/31 3:16:20

UI设计中箭头移动原理与实现技术详解

1. 箭头移动原理的基本概念箭头移动原理是一种广泛应用于用户界面设计、动画制作和交互设计领域的基础技术。简单来说&#xff0c;它描述了箭头或其他指示性元素在屏幕上移动时的行为规律和视觉表现方式。这个看似简单的概念背后&#xff0c;其实蕴含着丰富的设计哲学和人机交互…

作者头像 李华
网站建设 2026/10/31 3:13:55

2026年大模型API聚合平台架构与选型指南

1. 2026年大模型API聚合平台的行业背景与核心价值2026年的大模型技术格局已经呈现出明显的"三足鼎立"态势&#xff1a;以OpenAI为代表的闭源商业大模型、以Llama系列为代表的开源社区大模型&#xff0c;以及各大科技公司自研的垂直领域大模型。这种技术分化直接催生了…

作者头像 李华
网站建设 2026/10/31 3:13:06

界面渲染原理与Web性能优化实践

1. 界面渲染的核心概念解析界面渲染是现代计算机图形学中最基础也最重要的技术之一&#xff0c;它指的是将抽象的界面描述&#xff08;如HTML/CSS、UI组件树等&#xff09;转换为最终呈现在屏幕上的像素图像的过程。这个过程涉及到多个技术层面的协同工作&#xff0c;包括布局计…

作者头像 李华
网站建设 2026/10/31 3:11:18

MySQL 8.0 免安装版实战指南:解压即用、可审计的数据库运行时

简介&#xff1a;本资源是MySQL 8.0的绿色免安装版&#xff0c;面向数据库初学者、开发测试人员及需快速搭建本地环境的工程师&#xff0c;解决传统安装流程繁琐、配置复杂、易与系统已有服务冲突等问题。压缩包为ZIP格式&#xff0c;共420个文件&#xff0c;涵盖98个DLL&#…

作者头像 李华
网站建设 2026/10/31 3:10:53

机器人动作捕捉与仿生学习技术解析

1. 项目概述&#xff1a;当机器人学会"模仿秀"去年在东京机器人展上&#xff0c;我看到一个令人震撼的场景&#xff1a;操作人员做出太极拳的云手动作&#xff0c;三米外的Atlas人形机器人几乎同步完成了相同动作&#xff0c;连衣袖摆动的幅度都分毫不差。这种精准的…

作者头像 李华