271
社区成员




就我个人而言,本单元的测试做的还是比较草率的。前两次作业完全是通过自己构造一些数据进行的。但显然这种方式的效率是很低的,很容易给人测了几组数据就觉得没什么问题的幻觉。所以后来借鉴了本单元的Junit的测试,自己对一些较为复杂的方法都构建了相应的单元测试,并且借助AI的力量构造了不少边缘的测试样例,这在自我检测和互测中都起到了不小的作用。
我的数据构造策略其实就是特殊加许多一般。
public class SendMessageTest {
private Network network;
private Person p1, p2, p3;
private Tag tag;
private OfficialAccount account;
private Random rand = new Random();
private ArrayList<Person> people = new ArrayList<>();
private ArrayList<Tag> tags = new ArrayList<>();
@BeforeEach
void setUp() throws Exception {
}
// ------------------------ 类型0消息测试 ------------------------
@Test
void sendType0_RedEnvelopeMessage() throws Exception {
}
@Test
void sendType0_EmojiMessage() throws Exception {
}
// ------------------------ 类型1消息测试 ------------------------
@Test
void sendType1_ForwardMessage() throws Exception {
}
@Test
void sendType1_RedEnvelopeToTag() throws Exception {
}
// ------------------------ 异常测试 ------------------------
@Test
void sendInvalidMessageId() {
}
@Test
void sendType0_NoRelation() throws Exception {
}
@Test
void sendType1_InvalidTag() throws Exception {
}
@RepeatedTest(100)
void randomMessageTest() throws Exception {
randomSetUp();
// 随机选择消息类型(0或1)
int messageType = rand.nextInt(2);
// 生成消息内容
Message msg = generateRandomMessage(messageType);
int msgId = msg.getId();
// 记录初始状态
StateSnapshot snapshot = captureSystemState(msg);
network.addMessage(msg);
try {
network.sendMessage(msgId);
// 验证消息已移除
assertFalse(network.containsMessage(msgId));
// 类型相关验证
if (messageType == 0) {
verifyType0Effects(msg, snapshot);
} else {
verifyType1Effects(msg, snapshot);
}
} catch (Exception e) {
// 异常后验证状态不变
throw e;
}
}
private Message generateRandomMessage(int type) throws Exception {
}
private Person randomPerson() {
}
private Person getLinkedPerson(Person sender) {
}
private int storeRandomEmoji() throws Exception {
}
private int contributeRandomArticle(Person sender) throws Exception {
}
private void verifyType0Effects(Message msg, StateSnapshot snapshot) throws Exception {
}
private void verifyType1Effects(Message msg, StateSnapshot snapshot) throws Exception {
}
private void randomSetUp() throws Exception {
}
private static class StateSnapshot {
HashMap<Integer, Integer> socialValues = new HashMap<>();
HashMap<Integer, Integer> money = new HashMap<>();
int emojiHeat;
HashMap<Integer, List<Integer>> articles = new HashMap<>();
}
}
以上代码展示了我对sendMessage方法进行的各类测试,包含了各种类型消息的测试(特殊测试),同时也进行了随机性测试(许多一般)。具体内容鉴于篇幅问题就删去了。
综上所述,其实就是针对上面的单元测试,功能测试,集成测试,压力测试和回归测试来进行全面的测试(形式上的全面),我们在构造的时候主要就是针对功能和压力两个方面来进行,完成好这两项测试,我们的程序就不会出现很大的问题。
本单元的规格化设计非常容易让人看的头昏眼花,这时合理的使用大模型就成为了我们提高效率的一大利器。对此我总结了以下经验:
PS:我个人使用大模型的时候,针对deepseek和chatGPT会有不同的使用风格。前者我会更倾向于当一个工具来使用,后者我会倾向于当一个朋友那样唠嗑使用。因为GPT现在回答问题非常的俏皮,不太忍心当工具使唤(乐)。
其实本单元的构建和维护策略的决定并不在个人,而是在提供的各种接口上。分析整个network的构建,其实主要操作都是集中在Network类里面的,NetworkInterface为我们提供了各式各样的操作,添加person(添加结点),添加relation(添加边),添加tag,officialAccount,Message等等操作。
如果只谈对整个network图的构建的话,我们只需要关注addPerson,addRelation,modifyRelation即可。在社交网络中,每一个独立的person就是一个节点,每当我们添加一个person,整张图就多一个节点,每当我们添加一对relation,这个图就多了一条边(无向,有权),modifyRelation会稍微复杂一些,如果边的权重大于0就只是改权重即可,如果权重小于0,那么我们就需要删掉这条关系(注意,虽然我们前面说这条边是无向的,其实我们在具体维护的时候是按照类似双向边的操作进行的,所以我们应该对二者的acquaintance同时进行删除操作),同时还要对tag进行修改(断绝一切关系)。以上操作其实都是按照JML的内容进行的,并没有什么个人的发挥空间,只需要按照JML一步一步完成,把ensures的内容都完成就OK。下面是展示的具体实现过程:
public void addPerson(PersonInterface person) throws EqualPersonIdException {
if (containsPerson(person.getId())) {
throw new EqualPersonIdException(person.getId());
}
if (person instanceof Person) {
Person person1 = (Person) person;
persons.add(person1);
}
}
public void addRelation(int id1, int id2, int value)
throws PersonIdNotFoundException, EqualRelationException
{
handleNoPersonException(id1);
handleNoPersonException(id2);
Person person1 = getPerson(id1);
Person person2 = getPerson(id2);
if (person1.isLinked(person2)) {
throw new EqualRelationException(id1, id2);
}
person1.addAcquaintance(person2);
person2.addAcquaintance(person1);
person1.addValue(person2, value);
person2.addValue(person1, value);
//维护tripleSum变量
tripleSum += calcTripleSum(person1, person2);
}
public void modifyRelation(int id1, int id2, int value)
throws PersonIdNotFoundException, EqualPersonIdException, RelationNotFoundException
{
handleNoPersonException(id1);
handleNoPersonException(id2);
if (id1 == id2) {
throw new EqualPersonIdException(id1);
}
Person person1 = getPerson(id1);
Person person2 = getPerson(id2);
if (!person1.isLinked(person2)) {
throw new RelationNotFoundException(id1, id2);
}
int oldValue = person1.queryValue(person2);
if (oldValue + value > 0) {
person2.modifyValue(person1, oldValue + value);
person1.modifyValue(person2, oldValue + value);
} else {
person1.delValueAndAcquaintance(person2);
person2.delValueAndAcquaintance(person1);
person1.delRelationInTag(person2);
person2.delRelationInTag(person1);
//维护tripleSum变量
tripleSum -= calcTripleSum(person1, person2);
}
}
我们发现对于构建图而言是相当简单的,但是除了构建操作外,还有对图的访问操作,这个是相对困难的。我们需要考虑最短路径,有没有环路,三元环数量等等。这些操作单独看并不复杂,但是对于大数据量的图来讲,如果选用多重循环这种操作的话,很容易就超时,所以我们需要采用更加聪明的办法来解决,比如说动态维护一个变量这样的操作,虽然并没有改变图的形状,但是我们的确是在维护一个和这个图紧密相关的变量,欲知具体实现如何,请看下一个部分。
上一个部分讲到对图的访问操作很容易就发生超时的问题,如果我们直接按照JML去写就大概率会出问题。比如:
/*@ ensures \result ==
@ (\sum int i; 0 <= i && i < persons.length;
@ (\sum int j; i < j && j < persons.length;
@ (\sum int k; j < k && k < persons.length
@ && getPerson(persons[i].getId()).isLinked(getPerson(persons[j].getId()))
@ && getPerson(persons[j].getId()).isLinked(getPerson(persons[k].getId()))
@ && getPerson(persons[k].getId()).isLinked(getPerson(persons[i].getId()));
@ 1)));
@*/
public /*@ pure @*/ int queryTripleSum();
就这个获取三元环的操作,这三重循环绝对用不得。那我们就要想办法用更加高效的方式,比如动态维护一个tripleSum变量,我们需要考虑什么时候会产生或减少三元环,显然就是addRelation和modifyRelation(具体就是添加和删除关系的时候),addRelation时,我们通过判断添加的两个节点的共同朋友的个数来判断最后添加的三元环的个数,这很显然。modifyRelation时,如果是删除关系的话,我们也是通过判断共同的朋友来判断删除三元环的个数。(在上一个部分的modifyRelation方法中展示了这两个操作)后面还有类似的操作也是通过动态维护变量的操作来改变复杂度的。
通过上面举的例子,我们就很好理解什么是规格与实现分离了,规格限制的是我们的过程吗?不是。规格限制的是最后的结果!我们只需要让我们的方法的返回的结果符合最后规格所描述的限制即可。还是以上面为例,规格在说什么?在说“我希望结果是三元环的数量”,所以我们只需要最后返回正确的三元环数量就好了,我不管你具体怎么实现的。所以我们这时就需要聪明一些,使用效率更高的方法来取代这个复杂的方法。
一句话总结,规格只限制最终的结果而不在意过程的实现。
本单元的Junit测试的编写因为JML规格而变得更好写了。
前面我们提到JML规格之限制了最终的结果,而没有限制过程,所以我们可以选择走捷径,但是如果捷径没走对就很容易出现bug,所以我们可以结合Junit进行检查。因为Junit并没有约束时间限制,我们完全就可以将规格的限制照搬到Junit中,然后利用规格的一定正确的限制去检查我们的实现是否有问题。这个是JML规格在测试我们用非规格限制实现的方法时的好处。
这时候有的同学可能会问,“我的实现方式和规格描述的一模一样,我该怎么检查呢?”,这确实值得考虑,我们总不能自己检查自己,这样毫无意义。其实这时候我认为我们只需要保证和JML规格一模一样就OK了,上几个特值样例进行校验即可,不必过多纠结。
对于较为复杂的规格限制,我们可以通过观察assignable来判断需要检查的对象,比如(下面是一段超长JML):
/*@ public normal_behavior
@ requires containsMessage(id) && getMessage(id).getType() == 0 &&
@ getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()) &&
@ getMessage(id).getPerson1() != getMessage(id).getPerson2();
@ assignable messages, emojiHeatList[*];
@ assignable getMessage(id).getPerson1().socialValue, getMessage(id).getPerson1().money;
@ assignable getMessage(id).getPerson2().messages,
@ getMessage(id).getPerson2().socialValue,
@ getMessage(id).getPerson2().money,
@ getMessage(id).getPerson2().receivedArticles;
@ ensures !containsMessage(id);
@ ensures \old(getMessage(id)).getPerson1().getSocialValue() ==
@ \old(getMessage(id).getPerson1().getSocialValue()) + \old(getMessage(id)).getSocialValue() &&
@ \old(getMessage(id)).getPerson2().getSocialValue() ==
@ \old(getMessage(id).getPerson2().getSocialValue()) + \old(getMessage(id)).getSocialValue();
@ ensures (\old(getMessage(id)) instanceof RedEnvelopeMessageInterface) ==>
@ (\old(getMessage(id)).getPerson1().getMoney() ==
@ \old(getMessage(id).getPerson1().getMoney()) - ((RedEnvelopeMessageInterface)\old(getMessage(id))).getMoney() &&
@ \old(getMessage(id)).getPerson2().getMoney() ==
@ \old(getMessage(id).getPerson2().getMoney()) + ((RedEnvelopeMessageInterface)\old(getMessage(id))).getMoney());
@ ensures (\old(getMessage(id)) instanceof ForwardMessageInterface) ==>
@ ((\old(getMessage(id)).getPerson2().getReceivedArticles().get(0).equals(((ForwardMessageInterface)\old(getMessage(id))).getArticleId())) &&
@ (\forall int i; 0 <= i && i < \old(getMessage(id).getPerson2().getReceivedArticles().size());
@ \old(getMessage(id)).getPerson2().getReceivedArticles().get(i+1) == \old(getMessage(id).getPerson2().getReceivedArticles().get(i))) &&
@ \old(getMessage(id)).getPerson2().getReceivedArticles().size() == \old(getMessage(id).getPerson2().getReceivedArticles().size()) + 1);
@ ensures (!(\old(getMessage(id)) instanceof RedEnvelopeMessageInterface)) ==> (\not_assigned(persons[*].money));
@ ensures (\old(getMessage(id)) instanceof EmojiMessageInterface) ==>
@ (\exists int i; 0 <= i && i < emojiIdList.length && emojiIdList[i] == ((EmojiMessageInterface)\old(getMessage(id))).getEmojiId();
@ emojiHeatList[i] == \old(emojiHeatList[i]) + 1);
@ ensures (!(\old(getMessage(id)) instanceof EmojiMessageInterface)) ==> \not_assigned(emojiHeatList);
@ ensures (\forall int i; 0 <= i && i < \old(getMessage(id).getPerson2().getMessages().size());
@ \old(getMessage(id)).getPerson2().getMessages().get(i+1) == \old(getMessage(id).getPerson2().getMessages().get(i)));
@ ensures \old(getMessage(id)).getPerson2().getMessages().get(0).equals(\old(getMessage(id)));
@ ensures \old(getMessage(id)).getPerson2().getMessages().size() == \old(getMessage(id).getPerson2().getMessages().size()) + 1;
@ also
@ public normal_behavior
@ requires containsMessage(id) && getMessage(id).getType() == 1 &&
@ getMessage(id).getPerson1().containsTag(getMessage(id).getTag().getId());
@ assignable messages, emojiHeatList[*];
@ assignable getMessage(id).getPerson1().socialValue, getMessage(id).getPerson1().money;
@ assignable getMessage(id).getTag().persons[*].messages,
@ getMessage(id).getTag().persons[*].socialValue,
@ getMessage(id).getTag().persons[*].money,
@ getMessage(id).getTag().persons[*].receivedArticles;
@ ensures !containsMessage(id)
@ ensures \old(getMessage(id)).getPerson1().getSocialValue() ==
@ \old(getMessage(id).getPerson1().getSocialValue())+ \old(getMessage(id)).getSocialValue();
@ ensures (\forall PersonInterface p; \old(getMessage(id)).getTag().hasPerson(p); p.getSocialValue() ==
@ \old(p.getSocialValue()) + \old(getMessage(id)).getSocialValue());
@ ensures (\old(getMessage(id)) instanceof RedEnvelopeMessageInterface) && (\old(getMessage(id)).getTag().getSize() > 0) ==>
@ (\exists int i; i == ((RedEnvelopeMessageInterface)\old(getMessage(id))).getMoney()/\old(getMessage(id)).getTag().getSize();
@ \old(getMessage(id)).getPerson1().getMoney() ==
@ \old(getMessage(id).getPerson1().getMoney()) - i*\old(getMessage(id)).getTag().getSize() &&
@ (\forall PersonInterface p; \old(getMessage(id)).getTag().hasPerson(p);
@ p.getMoney() == \old(p.getMoney()) + i));
@ ensures (\old(getMessage(id)) instanceof ForwardMessageInterface) && (\old(getMessage(id)).getTag().getSize() > 0) ==>
@ (\forall PersonInterface p; \old(getMessage(id)).getTag().hasPerson(p); p.getReceivedArticles().get(0).equals(((ForwardMessageInterface)\old(getMessage(id))).getArticleId())) &&
@ (\forall PersonInterface p; \old(getMessage(id)).getTag().hasPerson(p); (\forall int i; 0 <= i && i < \old(p.getReceivedArticles().size());
@ p.getReceivedArticles().get(i+1) == \old(p.getReceivedArticles().get(i)))) &&
@ (\forall PersonInterface p; \old(getMessage(id)).getTag().hasPerson(p); p.getReceivedArticles().size() == \old(p.getReceivedArticles().size()) + 1);
@ ensures (\old(getMessage(id)) instanceof EmojiMessageInterface) ==>
@ (\exists int i; 0 <= i && i < emojiIdList.length && emojiIdList[i] == ((EmojiMessageInterface)\old(getMessage(id))).getEmojiId();
@ emojiHeatList[i] == \old(emojiHeatList[i]) + 1);
@ ensures (\forall PersonInterface p; \old(getMessage(id)).getTag().hasPerson(p); p.getMessages().get(0).equals(\old(getMessage(id))));
@ ensures (\forall PersonInterface p; \old(getMessage(id)).getTag().hasPerson(p); (\forall int i; 0 <= i && i < \old(p.getMessages().size());
@ p.getMessages().get(i+1) == \old(p.getMessages().get(i))));
@ ensures (\forall PersonInterface p; \old(getMessage(id)).getTag().hasPerson(p); p.getMessages().size() == \old(p.getMessages().size()) + 1);
@ also
@ public exceptional_behavior
@ signals (MessageIdNotFoundException e) !containsMessage(id);
@ signals (RelationNotFoundException e) containsMessage(id) && getMessage(id).getType() == 0 &&
@ !(getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()));
@ signals (TagIdNotFoundException e) containsMessage(id) && getMessage(id).getType() == 1 &&
@ !getMessage(id).getPerson1().containsTag(getMessage(id).getTag().getId());
@*/
public /*@ safe @*/ void sendMessage(int id) throws
RelationNotFoundException, MessageIdNotFoundException, TagIdNotFoundException;
对于上面这段超长JML,我们在测试的时候关注assignable部分就可以帮助我们快速的确定我们需要检查的部分,而不需要一步一步去看这个方法改变了什么变量,添加了哪些新东西,删除了哪些部分,这是合理利用JML进行Junit编写给我们带来的一大好处。下面是我对这个理论的应用:
private void verifyType0Effects(Message msg, StateSnapshot snapshot) throws Exception {
for (Person p : people) {
if (p.equals(msg.getPerson2())) {
assertEquals(p.getSocialValue(), snapshot.socialValues.get(p.getId()) + msg.getSocialValue());
if (msg instanceof RedEnvelopeMessage) {
assertEquals(p.getMoney(), ((RedEnvelopeMessage) msg).getMoney() + snapshot.money.get(p.getId()));
}
if (msg instanceof ForwardMessage) {
assertEquals(((ForwardMessage) msg).getArticleId(), p.getReceivedArticles().get(0));
for (int i = 0; i < snapshot.articles.get(p.getId()).size(); i++) {
assertEquals(snapshot.articles.get(p.getId()).get(i), p.getReceivedArticles().get(i + 1));
}
}
continue;
}
if (p.equals(msg.getPerson1())) {
assertEquals(p.getSocialValue(), snapshot.socialValues.get(p.getId()) + msg.getSocialValue());
if (msg instanceof RedEnvelopeMessage) {
assertEquals(p.getMoney(), snapshot.money.get(p.getId()) - ((RedEnvelopeMessage) msg).getMoney());
}
continue;
}
assertEquals(p.getMoney(), snapshot.money.get(p.getId()));
assertEquals(p.getSocialValue(), snapshot.socialValues.get(p.getId()));
assertEquals(p.getReceivedArticles(), snapshot.articles.get(p.getId()));
assertEquals(p.getMoney(), snapshot.money.get(p.getId()));
if (rand.nextBoolean()) {
System.out.println("qsv" + " " + p.getId());
System.out.println("qra" + " " + p.getId());
System.out.println("qm" + " " + p.getId());
}
}
if (msg instanceof EmojiMessage) {
assertEquals(network.queryPopularity(((EmojiMessage) msg).getEmojiId()), snapshot.emojiHeat + 1);
System.out.println("qp" + " " + ((EmojiMessage) msg).getEmojiId());
}
}
仔细观察我的assert,你会发现,我判断的内容完全就是assignable所强调的内容,这一点JML规格的确帮了很大的忙,减轻了我个人的工作量。
JML规格带给我的收获不只是读懂语法,学会编写代码,还有谨慎和细心。前两次作业都是因为漏掉了JML的部分要求而导致最终强测分数很低,最后一次作业可谓是谨慎至极,先是自己检查了数遍,随后又让AI帮我检查了一遍,然后又编写了许多Junit测试来检测问题,虽然都没查出问题,看起来是没起到一点效果,但是这样的测试还是让我十分安心,最后强测结果也的确比前几次强了不少,只能说功夫不负有心人,耕耘不一定有收获,但是不耕耘绝对没有收获。
在写Junit测试的时候,前两次完全是无头苍蝇,完全不知道自己的数据为何不全面,而最后一次测试的时候,根据前两次的测试经验和AI的辅助,在耐心的思考和编写下一次便通过了Junit测试。
虽然这个单元的前两次强测分数都不高,但是最后一次有了极大的进步,这点令我很是兴奋(尴尬的是互测截至时间记错了,有几个自己编的好数据还没来得及测),我认为只要有所收获,收获的比别人多就是好事。并且在最近也看到一段话觉得很有道理“自然界起的最大的雾是骄傲”,当我们收获成果的时候绝对不要张扬,也没什么好炫耀的,继续专注自我,保持谦逊,怀揣着一颗学徒的心继续努力。