ooUnit3

JML规格化设计

前言

本单元作业是一个助教辛苦,学生幸福的单元,首先感谢助教对于JML的接口规格化设计。

第三单元测试过程

对测试的理解

  1. 单元测试,目标:验证最小功能单元(函数、类)是否按预期运行。
  2. 功能测试,目标:验证功能是否符合需求规格。
  3. 集成测试,目标:验证多个模块组合后的协同工作是否正常。
  4. 压力测试,目标:测试系统在高负载、大数据量下是否稳定、性能合理。
  5. 回归测试,目标:确保旧功能在新版本中仍然正确。

数据构造策略

  1. 状态跟踪使用 existing_person_ids、existing_relations 等集合保存已创建实体,确保生成的命令多数为有效命令;避免了生成无效命令浪费测试机会。

  2. 概率控制 每次生成 ID 时有 80% 以上概率使用已存在的 ID,20% 左右概率生成不存在的 ID,用于测试非法输入处理; 保证测试“有效性 + 鲁棒性”兼备。

  3. 模块覆盖与比例平衡明确控制三大模块(HW1、HW2、HW3)的命令分布权重;比如HW3权重更高(如 am、aem),适应后期重点测试。

  4. 边界情况生成比如 ID 范围为 [-100, 100],年龄范围为 [1, 200]; 使用不同数据长度(如随机生成长度 1~100 的名字)。

  5. 指令生成路径,按照一定的策略随机的生成指定数量的指令,负责测试程序的不同功能。

大模型辅助

一、整体策略:从“提问者”到“指挥者”

大模型的强项是语言理解与上下文记忆,而不是通灵。要发挥其最大效能,关键在于你如何设计任务结构、输入信息与交互节奏。可遵循以下三步法:

  • 目标清晰(What)明确告诉模型你要做什么,如:“我正在开发一个评测机,请帮我生成合理的测试用例生成器。”

  • 约束充分(How)提供必要背景与规则:输入格式、限制条件、边界值、接口定义、已有类说明等。

  • 拆解合理(Step by step)将复杂任务拆解为子任务,让模型逐步完成(如先写接口草图,再实现核心函数,最后补充状态维护)。

二、不同典型场景下的引导技巧

场景一:规格化设计(接口与架构)

目标:设计合理的类、方法、数据结构

引导技巧:

明确提出功能要求(如“实现一个支持动态添加好友、标签的社交网络类”)

说明输入输出格式(如命令格式 ap id name age)

提供已有组件(如你已有的 Person 类定义)

可用 prompt 模板示例:

1
2
3
4
我希望你帮我设计一个 Java 类 `Network`,它支持以下命令:
- `ap id name age`:添加用户
- `ar id1 id2 val`:建立关系
这个类应该如何设计接口?需要哪些内部数据结构?

场景二:代码实现(核心算法、状态维护)

目标:写出能工作的函数/类

引导技巧:

提供函数签名与预期行为(如“输入为命令列表,输出为处理结果”)

提供已有代码片段、类定义供调用

可示例性描述一些关键输入输出

若模型写的第一版不够精确,可反复对细节提问并要求“逐行解释你的代码”

场景三:测试与验证

目标:构造测试用例,验证正确性

引导技巧:

给出你期望测试的函数或命令格式(如 “qv id1 id2”)

明确边界(如“我要测试最大长度字符串输入”)

命令示例 + 状态约束(如“已存在的 ID 集合是 {1, 2, 3},请生成合法但边界的 mr 命令”)

场景四:优化与分析

目标:让模型分析瓶颈、重构代码或提出设计优化建议

引导技巧:

提供当前实现代码 + 描述性能瓶颈

提出明确优化目标(如“将当前 getValueSum() 从 O(n) 降为 O(1)”)

要求模型分析时提供理由,不仅是结论

关键数据结构和算法设计

hw9

Person:

1
2
3
4
5
6
7
8
9
public class Person implements PersonInterface {
private String name;
private int age;
private int id;
private HashMap<Integer, PersonInterface> acquaintance;
private HashMap<Integer, PersonValuePair> values;
private HashMap<Integer, TagInterface> tags;
private TreeSet<PersonValuePair> ts = new TreeSet<>();
}

为了方便查询acquaintance,使用了复杂度为O(1)的HashMap来存储Person的好友。

本次作业中,Person需要注重考虑优化getBestAcquaintance()方法,于是我采用了java自己的treeSet来实现O(logn)维护,O(1)查询,只需要重写Set中元素的compareTo()方法即可线上维护value的顺序。

Tag:

1
2
3
4
5
6
public class Tag implements TagInterface {
private int id;
private HashMap<Integer, PersonInterface> persons;
private int sum = 0;
private int sum2 = 0;
}

本次作业Tag需要注意的点是getAgeMean()和getAgeVar(),本人同样实现了O(1)维护,O(1)查询的方法,只需要维护好value和以及value^2和即可。

NetWork:

1
2
3
4
public class Network implements NetworkInterface {
private HashMap<Integer, PersonInterface> persons;
private int triple = 0;
}

NetWork的难点主要体现在查询三元环数量以及连通性查询

关于三元环的查询,我选择维护一个triple变量,在每一次加边和删边操作时进行O(N)的维护,实测效果很好。

关于连通性查询,我见许多同学使用了并查集这个数据结构,但是本次作业中涉及到了删边操作,但是并查集并不支持删边后的查询,于是只能重建,鉴于实现起来过于复杂,我选择了BFS查询连通性,这一算法在后续接口中也会使用,只能说性价比极高。

hw10

新增类OfficialAccount:

1
2
3
4
5
6
7
8
9
public class OfficialAccount implements OfficialAccountInterface {
private int ownerId;
private int id;
private String name;
private HashMap<Integer, PersonInterface> followers;
private HashMap<Integer, PersonInterface> articles;
private HashMap<Integer, PersonValuePair> contributions;
private TreeSet<PersonValuePair> ts;
}

这是本次作业新增的类OfficialAccount,主要负责维护公众号的关注者和文章(仅关注ID)以及相应关注者的贡献。

需要注意的地方只有一个接口getBestContributor(),所以我们选择和上次作业一样的做法,用TreeSet来维护贡献的有序性,然后就可以很快的查询最佳贡献者了。

Person改动:

1
2
3
4
5
6
public class Person implements PersonInterface {
private TreeMap<Integer, Integer> receivedArticles;
private HashMap<Integer, Integer> articles;
private HashMap<PersonValuePair, TagInterface> otherTags;
private int order = 0;
}

我们需要维护Person收到的文章以及他们接受的顺序,由于在这一次作业中不可能出现一个人多次接受同一篇文章,所以我们用Map来存储article。

但是问题来了,我们怎么维护文章的接受顺序呢?没错,又是TreeMap,我们在Person里维护一个取号机制,来一篇文章取一个号,同时号码减一,然后利用TreeMap能维护Key的顺序的机制,我们就能维护好文章的顺序啦,先进来的文章取得号一定大于后进来的文章。

我们还注意到我维护了一个otherTags,这其实是为了Network里的一个接口服务的,这里暂不讲解。

Tag改动:

Tag新增了一个对于内部Person之间value和的查询,但是这个实现主要是在Network中维护的,所以不在这里讲解了。

Network改动:

新增了双元环数量的查询(当一对Perosn互为最好的朋友时就视为一个双元环),这个维护方法和三元环类似,主要是在网络中加边删边时正确更改即可。

新增了对于TagValueSum的查询,注意到我在Person里维护了Person被归类的Tag,这样就可以在更改关系时查询会涉及到哪些Tag的valueSum,然后一一线上维护即可。

新增了最短路径的接口,我们采用了BFS的实现,简单高效。

hw11

本次作业的改动主要为新增了Message类,Message承担了转发文章(被转发的人会接受),发红包(更改Person的Money值),发表情包的作用,同时会增加转发于被转发者的社交值。

在时间复杂度上没有什么难点,只需要注意处理机制就行,因为这次作业的JML超长!!!!

从规格到实现,性能如何保障?

JML设计者在写规格时想的一定不是具体的实现方法,而是怎么去简单又完美的约束好一个接口的功能与副作用,是站在整体的角度思考问题的,想好了一切可能的行为,分别做好不同的约束处理。

实现则是在遵守JML约束的前提下,尽可能的提高实现的性能。此时工作人员想的是一个具体的实现方法。

JUNIT测试

一、JUnit 测试的本质与作用

JUnit 是 Java 中最常用的单元测试框架,主要用于:

  • 自动化验证类和方法的行为是否符合预期规格;

  • 支持回归测试,及时发现代码修改带来的错误;

  • 增强测试覆盖率(代码分支、边界、异常情况);

  • 与规格(如 JML)结合时,充当规格实现一致性的验证器。

二、利用“规格信息”设计 JUnit 测试的思路

  1. 明确每个方法的规格(前置条件、后置条件、不变量)设计测试用例时:依据每条规格构造一个正例 + 至少一个反例。
  2. 将规格翻译为断言(assert)JUnit 提供 assertEquals, assertTrue, assertThrows 等断言方法。
  3. 结合边界值与等价类分析基于规格设计测试数据,覆盖各类输入场景。

三、JUnit 测试验证规格一致性的效果体现

  1. 捕捉与规格不一致的实现错误如:
  • 规格要求“相同关系不可重复添加”,但实现却允许重复;

  • JML 声明“添加后 size 增加”,但实际调用无变化。

通过 JUnit 断言对比前后状态,可快速识别这些逻辑偏差。

  1. 提高可维护性与回归保障一旦方法规格变更,只需更新对应测试:避免因手动测试遗漏导致旧功能失效;结合 CI/CD,可以自动在提交时运行测试,保障规格长期一致性。

  2. 支持规格驱动开发(Test-first / TDD)JUnit 测试与规格结合,可倒逼实现更加规范:先写规格 → 写测试用例 → 最后写实现,类似于“契约驱动开发”,提升代码清晰度与可预测性。

四、实际项目中的 JUnit 设计示例

假设你有如下 JML 规格定义:

1
2
3
4
5
/*@ requires id > 0;
@ ensures \result != null;
@ ensures \result.getId() == id;
@*/
public Person getPerson(int id);

可设计如下 JUnit 测试:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testGetPersonNormal() {
network.addPerson(1, "Alice", 20);
Person p = network.getPerson(1);
assertNotNull(p);
assertEquals(1, p.getId());
}

@Test
public void testGetPersonInvalidId() {
assertThrows(IllegalArgumentException.class, () -> network.getPerson(-1));
}

学习体会

本单元的学习让我真正体会到“规格先行”的重要性。JML 规格不仅仅是对接口行为的约束,更是一种程序设计的思维方式。它迫使我在写代码前先理清楚每一个函数的职责、边界与副作用,从而避免“边写边试、边改边错”的低效流程。

在实现过程中,面对复杂接口和状态维护时,我深刻认识到:良好的设计是性能和正确性的保障。比如在 Person 类中使用 TreeSet 优化最优好友查询、在 Tag 中用数学方法实现均值与方差的 O(1) 维护、以及在 Network 中用 BFS 替代复杂的连通性维护方案——这些都是在规格指引下不断优化的结果。

此外,借助大模型的协作能力,我从“提问者”成长为“任务设计者”。我学会了如何清晰地表达意图、分步拆解任务、构建提示模板,并通过反复调试与细节提问来获得更贴合需求的输出。这种交互方式不仅提升了效率,也让我意识到人工智能更像是一个“逻辑助理”,它不会替代你的判断,而是在你的设计之下发挥最大价值。

最后,JML + JUnit 的组合是一次理论与实践的双重磨砺。它让我意识到代码的正确性从来不是“写完跑一下就行”,而是一种自我约束、一种契约精神。测试不是验收,而是开发的一部分,它推动我不断思考:我的代码,是否真正做到了“言出法随”?

这个单元虽然挑战很多,但正因为如此,我收获的也更多。感谢助教提供的精细规格,感谢这次设计-实现-测试一体化的宝贵锻炼机会。