目录
测试
黑箱测试
黑箱测试是指测试人员在不考虑程序内部逻辑和结构的情况下,仅通过程序的输入和输出来测试其功能,而不关心其内部实现细节。举个例子,比如输入为1,输出为5,利用黑箱测试只能对输入和输出进行测试,我们并不知道程序内部是经历了复杂的运算才输出5还是打点if-else输出5。黑箱测试虽然简单直观,但可能遗漏内部错误,测试效率较低。
白箱测试
白箱测试指测试人员具有对程序内部逻辑、结构和实现细节的完整知识,而不是像黑箱测试那样只关注于输入输出。白箱测试的目标是确保程序内部的所有逻辑路径都按照预期进行,并且所有的代码都被测试过,通常通过逻辑覆盖(如语句覆盖、判定覆盖、条件覆盖、路径覆盖等)来实现。与黑箱测试相比,白箱测试的测试效率更高,但设计和执行白箱测试用例通常需要大量的时间和资源。
单元测试
单元测试是软件开发中针对程序模块(单元)来进行正确性检验的测试工作,通常由开发人员进行编写。单元测试的目的在于检验程序基本组成单位的正确性,应该在开发过程中尽早进行,以便尽早发现和修复问题,有助于提高代码质量、提高可维护性。例如在本单元hw9,hw10,hw11中都有涉及对指定方法进行Junit编写,测试数据要充分考虑到可能出现的问题,并且Junit要覆盖每一个ensure和pure语句,防止出现漏网之鱼。
功能测试
是一种黑箱测试方法,它主要关注于验证软件系统的功能是否按照需求规格说明书或用户手册的要求正确实现。功能测试是确保软件系统满足用户需求和预期行为的关键步骤,通过模拟用户操作来检查系统的功能。
集成测试
与单元测试相反,集成测试不再关注于单个方法或类能否正确运行,而是验证不同的软件模块、组件或子系统在集成后是否能够按照设计要求协同工作,并达到预期的行为和结果。
压力测试
压力测试用于评估系统或组件在面临极端或超出正常操作负载时的性能和可靠性。它旨在模拟实际应用中可能出现的各种高负载情况,以检测系统的稳定性、可扩展性和容错能力。在本单元中就出现了许多复杂度比较高的方法,例如hw9中的isCircle方法,hw10中qcs,qtvs等方法,如果不对其进行特殊优化,在一般情况下是可以正确运行并且通过评测机检验的,但是在强测和互测中专门针对此类方法构造的数据就会使程序运行超时。
回归测试
回归测试指在原有的程序发生更改后,通过测试检验软件的修改部分有没有破坏已有的功能, 从而保证在软件开发周期中的稳定性和可靠性。
数据构造策略
在本单元中,我都数据构造策略更多关注于数据的全面性,构造的数据覆盖所有的指令,保证各个指令能够正确运行,输出对应的结果,类似于上文中的功能测试。
在Junit中我则是考虑了各种边界情况以及较复杂的数据,比如在构造的Network中加入一个和其他person没有任何联系的person,或者使所有person都两两有关联,重点考虑一些极端的情况。
但是我忽略了数据范围可能引发的错误,由于我在作业中使用int类型来存储person的id并且使用了id1-id2的比较器,所以在hw10互测中就被卡爆int范围了,非常惨痛的教训。
架构设计
我在写作业之初并没有思考在宏观上具体如何实现,只是单纯照着JML进行编写,但是本单元在很大程度上类似一个社交网络,所以理解起来较为容易。
这是我在hw11实现后的代码架构,主体部分即为MyNetwork、MyPerson、MyTag、MyMessage等以及Main类,Disjoint类是我在代码优化中使用了并查集,单独构建出一个类,使得代码封装性更强,exceptions文件夹下是按要求实现的各种异常。
图模型构建
社交关系网络可以很容易地和图联系起来,Network自身就是一个图,每个Person作为Network中 的结点,就Relation而言Network是双向图,而对于Message又是单向图,整个Network类按照JML编写即可实现。
但是在本单元中唯一的Network之外,我还加入了DisjointSet并查集模型,其目的是针对于hw9中的isCircle方法,核心思想是为所有直接相连或者间接相连的Person建立一个集合,并为之设立一个代表元用来表示这些相连(直接或间接)Person属于哪个集合。Disjoint类中的具体方法如下:
public void add(int id) {
if (!pre.containsKey(id)) {
pre.put(id, id);
blockNum++;
}
}
public int find(int id) { //返回id的代表元
int rep = id; //代表元
while (rep != pre.get(rep)) {
rep = pre.get(rep); //找到该id的代表元
}
int now = id;
while (now != rep) { //如果now不等于rep
int fa = pre.get(now); //找到now的父节点
pre.put(now, rep); //now的父节点也放到(now, rep)里,覆盖原有的结构,从而进行路径压缩
now = fa;
}
return rep;
}
public int merge(int id1, int id2) {
int fa1 = find(id1);
int fa2 = find(id2);
if (fa1 == fa2) { //说明id1与id2是同一个代表元,所以无需merge
return -1;
}
//如果id1与id2不是同一个代表元,此时需要merge
pre.put(fa1, fa2);
blockNum--;
return 0;
}
public void remove(int id1, HashSet<Person> persons1, int id2, HashSet<Person> persons2) {
pre.put(id1, id1); //将id的代表元设置为id自身
if (!persons1.isEmpty()) {
for (Person person : persons1) {
int newId = person.getId();
pre.put(newId, id1);
}
}
pre.put(id2, id2);
if (!persons2.isEmpty()) {
for (Person person : persons2) {
int newId = person.getId();
pre.put(newId, id2);
}
}
blockNum++;
}
在每次add_person或者modify_relation中都对Disjoint进行维护,这样做的好处就是可以在query_circle和query_block_num指令中将复杂度降为O(1)。
对于query_triple_sum、query_tag_value_sum、query_best_acquaintance等指令也分别使用了动态维护的思想,将这些方法的复杂度均摊到add_person、modify_relation等指令上,至于query_tag_age_var等Tag类内部的方法也是做到每次修改时都进行维护,真正需要调用这个指令的时候只需要直接读取结果即可。
性能问题及修复情况
在hw9中我对所有高复杂度的方法都是用动态维护进行了优化,使程序中所有方法的复杂度最高不超过O(n)。hw10中偷了个懒,没有对query_couple_sum和query_tag_value_sum进行动态优化,所以在强测中出现了CTLE问题。在bug修复环节,我采用了上文中提到的动态维护,在每次add_person和modify_relation的时候都会去调整,例如我在addRelation方法中的使用了大量的动态维护,因为绝大多数查询指令都和add_relation脱不了干系。
public void addRelation(int id1, int id2, int value)
throws PersonIdNotFoundException, EqualRelationException {
if (!containsPerson(id1)) {
throw new MyPersonIdNotFoundException(id1);
} else if (!containsPerson(id2)) {
throw new MyPersonIdNotFoundException(id2);
}
MyPerson myPerson1 = (MyPerson) persons.get(id1);
MyPerson myPerson2 = (MyPerson) persons.get(id2);
boolean isLinked = myPerson1.isLinked(myPerson2);
if (isLinked) {
throw new MyEqualRelationException(id1, id2);
}
final int bestId1 = myPerson1.getBestId();
final int bestId2 = myPerson2.getBestId();
myPerson1.addLink(myPerson2, value);
myPerson2.addLink(myPerson1, value);
myPerson1.getId2value().put(myPerson2.getId(), value);
myPerson2.getId2value().put(myPerson1.getId(), value);
myPerson1.getMaxHeap().add(myPerson2);
myPerson2.getMaxHeap().add(myPerson1);
coupleSum = tool.updateCoupleSumAdd(myPerson1, myPerson2, bestId1, bestId2,
coupleSum, persons);
tool.updateTagValueSum(id1, id2, 0, tags);
disjointSet.merge(id1, id2); //将id1和id2在并查集中进行merge
//动态维护三元环的个数
map.get(id1).add(id2);
map.get(id2).add(id1);
for (Integer i : map.get(id1)) {
if (map.get(id2).contains(i)) {
tripleSum++;
}
}
}
规格与实现的分离
我认为规格更像是换了一副面孔的指导书,限定了我们需要完成哪些内容,但是对于具体实现并没有做过多的阐述,例如我们可以根据自己的需要来使用不同的容器如HashMap、ArrayList、PriorityQueue等。但不管我们如何具体地去实现要求,我们都需要保证规格中涉及的内容如pure、ensure等要被满足。从某种层面上将,规格与实现像是甲方与乙方的关系,规格不必在意如何具体去实现,但是实现需要且必须满足规格中提的要求。
Junit测试心得
本单元的三次代码编写任务都涉及Junit测试。在Junit测试中需要着重考虑四个点:require、ensure、pure和构造数据。前三个点大都好理解,JML规格中给出的要求统统完成就好了,但是对于构造数据却是另一种感觉,哪怕你检测了所有JML给出的要求,但是仍然可能出现Junit部分未通过的现象,所以我们要构造有一定复杂度并且考虑极端情况的数据,比如我在前文中提到的孤立点,在hw10Junit测试中就是一个不得不考虑的问题。
学习体会
学之前:听说这单元挺简单的。
学之后:被揍得最痛的一集。
JML语言虽然看起来有些冗长,并且可读性远远没有前两个单元的作业指导书易于理解,而且直到自己实现了全部的方法,甚至有些难以理解自己是在干嘛。但是JML也具有不一样的优势,JML对于方法描述的严谨程度是自然语言难以比拟的,在工程中具有重大作用。这个单元似乎没有前几个单元深邃难以理解,但是在局部算法中却很让人头疼,从hw9的并查集到hw10中的bfs以及其他涉及动态维护的方法,对于实现提出了较高的要求。