271
社区成员




单元测试:
每个类(如 Network、Message、Person)的功能都通过独立的测试方法进行验证。
例如,在 NetworkTest.java 中,针对 deleteColdEmoji(int limit) 方法设计了多个测试用例,分别验证删除热度低于阈值的 emoji、limit=0 的边界情况以及无 emoji 数据的情况。
单元测试强调对单个函数或模块的隔离测试,确保每个组件在隔离环境下行为正确。
功能测试:
验证整个程序是否符合需求规格说明书(Spec)定义的行为。
例如,测试 addRelation() 是否正确地建立关系、是否能正确抛出异常、是否更新连通块和三元环等。
功能测试通常不关心内部实现,而是关注外部接口的输入输出是否符合预期。
集成测试:
测试不同模块之间的交互是否正确。例如,sendMessage() 可能涉及 Message、Person 和 Emoji
等多个类的协作。
在 testDeleteColdEmoji_RemoveLowPopularityEmojis() 中,不仅测试了 emojiHeatList 的清理,还检查了与之相关的 messages 是否被正确删除。
这种跨模块的数据一致性测试属于典型的集成测试范畴。
压力测试:
虽然未显式展示,但可以通过构造大量数据(如数万个 Person、Message)来模拟高并发场景,测试系统的吞吐量和响应时间。
压力测试可发现性能瓶颈,如某些算法的时间复杂度过高导致超时。
回归测试:
每次修改代码后重新运行所有测试用例,确保已有功能没有被破坏。
JUnit 提供了良好的支持,只需执行全部 Test 类即可完成回归测试。
测试类型 | 定义 | 特点 |
---|---|---|
单元测试 | 针对最小可测试单元(如函数)进行测试 | 快速、自动化、细粒度 |
功能测试 | 验证系统是否满足业务需求 | 黑盒视角、端到端 |
集成测试 | 验证多个模块组合后的交互逻辑是否正确 | 关注接口、依赖 |
压力测试 | 模拟高负载场景,测试系统极限性能 | 强调并发、资源占用 |
回归测试 | 修改代码后重新运行测试,防止引入新 bug | 自动化程度高、持续集成 |
边界数据:
构造最小/最大值,如 id = 0 或 Integer.MAX_VALUE。
如测试 deleteColdEmoji(0),验证当 limit 为 0 时不删除任何 emoji。
异常数据:
构造非法输入以触发异常,如重复的 ID、空对象、null 参数等。
例如在 addMessage(MessageInterface message) 中,传入 null 或非法 EmojiId
来测试异常处理。
典型数据:
构造正常业务流程下的数据,如添加多个用户并建立关系。
如 setUp() 中创建两个 Person 并建立关系,用于后续 sendMessage 测试。
随机数据:
使用工具生成大规模随机数据,用于压力测试或覆盖率提升。
可使用 Java 的 Random
或第三方库(如 JUnitParams)辅助构造。
覆盖分支数据:
根据代码结构构造数据,确保每条 if-else、switch-case 分支都被执行。
如在 sendMessage() 中,测试 type=0 和 type=1 两种情况,以及不同类型的 Message(如 RedEnvelope、Forward)。
历史 Bug 数据:
收集曾经修复过的 bug 所对应的输入数据,加入回归测试用例。
保证同类问题不再复现。
从单元到集成再到功能测试,层层递进,确保质量。
覆盖边界、异常、典型、分支等多类数据,提高测试覆盖率。
JUnit 等框架支持快速执行和回归测试。
采用多种策略构造数据,既能发现潜在缺陷,又能模拟真实场景。
根据案例中 MyJvm.createObject 方法 JML 规格的生成过程,总结引导大模型完成复杂任务的策略如下:
场景:需要同时处理堆空间充足/不足时的对象创建逻辑
引导方式:
先描述基础需求(创建对象+容量判断)
再补充触发 GC 后的对象保留规则
最后细化新旧对象关系的约束条件
示例:
// 第一层:基础容量判断
requires \old(heap.size) < (DEFAULT_CAPACITY - count)
ensures heap.size == \old(heap.size) + count
// 第二层:GC触发后的容量约束
requires \old(heap.size) >= (DEFAULT_CAPACITY - count)
ensures heap.size == (\sum ... ) + count
// 第三层:新旧对象引用关系
(\forall int i; ...)
场景:需要维护堆中对象的引用状态一致性
引导方式:
明确要求保留所有 referenced 对象
强调新对象必须被完整添加
约束未被引用对象的清除规则
关键 JML 片段:
ensures (\forall int i; 1 <= i && i <= \old(heap.size);
\old(heap.getElement(i).isReferenced()) ==>
(\exists int j; ...)) // 保留被引用对象
场景:需要区分正常创建与触发 GC 两种路径
引导方式:
使用 also
子句分离不同行为
通过 requires
明确前置条件边界
用 ensures
描述不同路径的后置状态
实现形式:
/*@ public normal_behavior
@ requires 条件1...
@ also
@ requires 条件2... */
场景:需要准确表达 JVM 垃圾回收语义
引导方式:
输入时明确 GC 的语义(如:只清除 unreferenced
对象)
提供 MyObject 类的关键属性(如 id 生成规则)
说明堆结构的特性(最小堆排序)
场景:处理大模型生成的初始规格不完整的情况
典型修正过程:
(\sum ...)
优化点:
使用 \forall
、\exists
量词明确对象关系
通过 \old()
准确标识初始状态
用 ==>
运算符描述条件逻辑
优势:
// 清晰表达引用对象保留规则
(\forall int i; ...
\old(...) ==> (\exists int j; ...))
通过以上策略,可系统性地引导大模型:
核心类结构:
Network:主控类,负责管理所有对象(Person、Tag、Message、Emoji等),实现社交网络的核心功能。
Person:表示用户节点,包含好友关系、标签、接收到的文章/消息等信息。
Tag:表示标签,用于组织多个用户并提供统计信息(如年龄均值、方差、valueSum)。
Message:消息抽象,支持点对点和广播两种类型,并派生出多种子类(RedEnvelope、Forward、Emoji)。
MyUtil:工具类,封装 BFS、最佳熟人查询等算法。
接口规范:
图模型构建方式:
图由 Person 节点构成,边为双向的 acquaintance 关系,用邻接表存储。
Network 维护连通块集合 (blocks) 和三元环数量 (qtsum),以支持快速查询。
每次添加/删除关系时都会动态更新图结构(如重新计算连通块、更新 bestAcquaintance)。
图模型维护策略:
连通性维护:使用 DFS 实现连通块划分,每次断边后重新生成连通块。
三元环维护:在添加/删除边时遍历可能形成新三角形的第三节点,维护 qtsum。
bestAcquaintance 动态维护:当关系变化时,触发重计算,保证每个用户的“最亲密好友”始终正确。
Tag 中 valueSum 的维护:通过 manageValueSum() 方法,在关系变更时同步更新 Tag 内部的 sum 值,避免重复计算。
问题 | 描述 | 影响 |
---|---|---|
O(N²) 遍历 | 如在 queryTagValueSum() 或 deleteBlock() 中频繁遍历所有用户或连通块 | 时间复杂度过高,影响大规模测试 |
标签 valueSum 同步更新不及时 | 多处修改关系未调用 manageValueSum(),导致查询结果错误 | 数据一致性问题 |
BFS 查询效率低 | 在 queryShortestPath() 中多次调用 BFS 导致超时 | 查询响应时间长 |
HashMap<Integer, Person>
存储用户,O(1) 查找;ArrayList<HashMap<Integer, Person>>
存储连通块,便于增删;HashSet<Integer>
存储文章 ID,O(1) 判重。规格定义清晰:
JML 规格明确约束了输入输出、异常抛出、不变量(invariant)、前置条件(requires)和后置条件(ensures)。
示例:@requires containsTag(id);
明确要求调用前必须存在该 tag,否则抛异常。
实现灵活多样:
不同开发者可以采用不同数据结构(如链表 vs 数组)、算法(DFS vs 并查集)实现相同接口。
只要满足 JML 规格,实现方式不影响外部调用者。
数据容器选择:
Tag 中使用 Person[] persons
存储成员,便于快速访问;
Network 中使用 List<Tag>
存储标签,方便增删改查;
Message 使用 Queue<Message>
缓冲待发送消息,提高并发处理能力。
动态维护机制:
如 Tag.valueSum 不在查询时计算,而是在每次关系变动时动态维护;
这种策略牺牲部分写入性能换取读取效率,适用于读多写少场景。
覆盖规格中的各种行为:
每个 @requires
条件都应有对应异常测试;
每个 ensures
条件都应有结果验证;
每个 invariant
都应在操作前后保持一致。
边界值测试:
测试空集合、单元素集合、最大值、最小值等边界情况;
如测试 deleteColdEmoji(0)、queryTripleSum() 在无三角形时返回 0。
状态变化测试:
测试方法执行前后系统状态是否符合预期;
如添加关系后检查连通块是否合并、bestAcquaintance 是否更新。
异常测试:
使用 assertThrows()
检查是否抛出正确异常;
如测试 addMessage() 传入非法 emojiId 是否抛出 EmojiIdNotFoundException。
集成测试:
多个操作组合后是否仍满足规格;
如先 addRelation(),再 modifyRelation(),最后 removeRelation(),最终状态是否回到初始。
一致性检验:
对比手动计算值与程序返回值是否一致;
如构造一个简单图,手动计算 shortest path,然后调用 queryShortestPath() 验证。
方面 | 效果 |
---|---|
异常检测 | 高效发现未处理的边界条件,如未检查 id 是否合法 |
数据一致性 | 有效识别 valueSum、qtsum 等字段更新遗漏或错误 |
行为一致性 | 确保不同实现方式下接口行为一致,如 equals() 、containsTag() |
覆盖率提升 | 通过构造典型、边界、异常数据,提高测试覆盖率 |
回归测试 | 修改代码后自动运行测试套件,防止引入新 bug |
文档辅助 | 测试用例可作为 API 使用示例,帮助理解接口用途 |
JML 与面向接口编程的重要性:
JML 提供了一种形式化的方式描述接口行为,极大提升了代码的可维护性和可扩展性;
接口与实现分离,使得多人协作开发更高效,降低模块耦合度。
图建模与动态维护技巧:
图结构是本单元的核心,如何高效构建和维护图模型是关键;
动态维护字段(如 bestAcquaintance、valueSum)需要合理设计触发时机,避免冗余计算。
性能优化意识增强:
面对大数据量时,简单的暴力算法容易超时;
必须结合数据结构特性、缓存机制、局部更新等方式进行优化。
测试驱动开发理念深入:
先写测试再编码有助于理清需求边界;
单元测试 + 集成测试双重保障,提升代码质量。
团队协作与规范统一:
在统一接口规范下,不同同学可独立开发不同模块;
测试用例共享机制提高了整个系统的健壮性。
本单元通过 JML 接口规范引导我们完成了一个完整的社交网络模拟系统,涵盖了图建模、动态维护、性能优化等多个方面。借助 JUnit 测试框架,我们不仅验证了代码与规格的一致性,也提升了测试驱动开发的能力。整个过程加深了我们对接口设计、图算法、数据结构优化的理解,是一次非常有价值的学习体验。