分析本单元的测试过程
黑箱测试
黑盒测试(Black Box Testing)是一种测试方法,测试人员在测试过程中只关注软件的输入和输出,而不考虑内部实现细节。测试人员对软件系统的功能和需求进行测试,通过输入测试数据并观察输出结果来验证系统是否按照预期工作。黑盒测试方法不需要了解软件的内部结构和代码,而是根据需求规格说明书、用户手册或其他文档进行测试。
白箱测试
白盒测试(White Box Testing),又称结构测试或透明盒测试,是一种测试方法,测试人员需要了解软件系统的内部结构和代码逻辑,以设计和执行测试用例。白盒测试关注的是软件系统的内部实现,通过检查代码覆盖率、路径覆盖等指标来评估测试的全面性和有效性。白盒测试可以帮助发现代码中的错误、逻辑缺陷和性能问题。
这两种测试方式在这个单元的测试中缺一不可,黑箱测试整体体现在利用评测机对程序的压缩文件与输入数据放在一起进行运行,测试出程序整体的正确性;而白箱测试是对于算法等具体代码进行的细致化测试,用于修改程序的错误或漏洞。
单元测试
用于验证代码中最小可测试单元(如函数、方法或类)的正确性。它的目标是通过独立地测试每个单元来确认其功能是否按预期工作。单元测试通常由开发人员编写,并在开发过程中进行。
功能测试
用于验证系统或应用程序的功能是否按照需求规格说明书中所定义的功能工作。功能测试关注的是系统的外部行为,以用户的角度来验证系统是否满足预期的功能需求。它可以通过手动测试或自动化测试脚本来执行。
集成测试
用于验证多个模块或组件在整合后是否正常工作。集成测试的目的是检测不同组件之间的接口问题、数据传递问题和协作问题等。它旨在保证各个组件之间的集成能够无缝协同,并且整个系统能够按照预期工作。
压力测试
压力测试(Stress Testing)是一种软件测试方法,用于评估系统在高负载情况下的性能和稳定性。压力测试通过模拟大量用户或高负载情况下的操作,向系统注入大量的并发请求,以测试系统在压力下的性能表现。
回归测试
用于确认在进行软件修改、修复错误或进行系统升级后,原有功能是否仍然正常运行。回归测试的目的是防止在修改现有代码时引入新的错误或导致原有功能发生故障。它通常在每次软件修改之后进行,以确保系统的稳定性和兼容性。
综合利用各种测试
作业只要求单元测试,其类似于功能测试,检验某个方法的正确性,为了验证我们程序的性能,有必要进行压力测试,此外,若进行的代码的优化与迭代,也有必要进行回归测试。
数据构造策略
首先,利用单元测试对复杂易错的方法进行独立测试,然后进行大量的随机测试解决简单的逻辑失误,最后对时间复杂度较高的指令进行压力测试。
架构设计
此图为最终架构图(由于图片过大,因此异常类放在另一张图片展示,如下)。
并查集
并查集(Union-Find)是一种数据结构,主要用于处理动态连通性问题。它能够高效地进行合并(Union)和查找(Find)操作。在这个单元的第一次作业中,判断两个点是否连通以及查询qbs等等都可以用并查集的思想高效实现,但由于该次作业中modifyrelation可能涉及删边操作,因此我们需要变通地采用部分并查集,当要删的边是两个连通子图的桥时,把原来1个block分裂为2个.实际操作如下:
public void manageBlock(Person p1, Person p2) {
int id1 = p1.getId();
int id2 = p2.getId();
HashMap<Integer, Person> block1 = new HashMap<>();
ArrayList<HashMap<Integer, Person>> blocks = network.getBlocks();
for (HashMap<Integer, Person> block : blocks) {
if (block.containsKey(id1)) {
block1 = block;
break;
}
}
if (block1.containsKey(id2)) {
return;
}
HashMap<Integer, Person> block2;
for (HashMap<Integer, Person> block : blocks) {
if (block.containsKey(id2)) {
blocks.remove(block);
block2 = block;
block1.putAll(block2);
break;
}
}
}
public void deleteBlock(int id1, int id2) {
HashMap<Integer, Person> dblock = null;
for (HashMap<Integer, Person> block : blocks) {
if (block.containsKey(id1) && block.containsKey(id2)) {
dblock = block;
break;
}
}
if (dblock != null) {
if (!isReachable(id1, id2)) {
HashMap<Integer, Person> newblock = new HashMap<>();
getOneBlock(id1, dblock, newblock);
if (!newblock.containsKey(id2)) {
dblock.keySet().removeAll(newblock.keySet());
blocks.add(newblock);
}
}
}
}
无向图
本人是严格按照JML规格实现,采用的是邻接表,不同的是,笔者用的是hashmap而非arraylist,这样可以高效get。使用HashMap
进行快速索引为了能将时间压缩到 $ O(1) $,这是必须的,因为索引方法几乎存在于所有方法中。
private HashMap<Integer, Person> people = new HashMap<>();
private HashMap<Integer, Message> messages = new HashMap<>();
private HashMap<Integer, Integer> emojiList = new HashMap<>();
private HashMap<Integer, Person> acquaintance = new HashMap<>();
private HashMap<Integer, Integer> value = new HashMap<>();
private HashMap<Integer, Tag> tags = new HashMap<>();
动态维护
动态维护对于查询占大多数的样例会显现出极大优势。
维护qtsum:在接受mr指令后可能出现删边的情况,需要调用以下方法维护qtsum三角形的数量。
public void deleteTriple(Person p1, Person p2) {
if (((MyPerson) p1).getAcquaintance().size() < ((MyPerson) p2).getAcquaintance().size()) {
for (Person p : ((MyPerson) p1).getAcquaintance().values()) {
if (!p.equals(p2) && p.isLinked(p2)) {
qtsum--;
}
}
} else {
for (Person p : ((MyPerson) p2).getAcquaintance().values()) {
if (!p.equals(p1) && p.isLinked(p1)) {
qtsum--;
}
}
}
}
在接受ar指令后可能出现删加边的情况,需要调用以下方法维护qtsum三角形的数量。
public void addTriple(Person p1, Person p2) {
if (((MyPerson) p1).getAcquaintance().size() < ((MyPerson) p2).getAcquaintance().size()) {
for (Person p : ((MyPerson) p1).getAcquaintance().values()) {
if (!p.equals(p2) && p.isLinked(p2)) {
network.addQtsum();
}
}
} else {
for (Person p : ((MyPerson) p2).getAcquaintance().values()) {
if (!p.equals(p1) && p.isLinked(p1)) {
network.addQtsum();
}
}
}
}
维护cpsum:在接受mr指令后可能出现删边,更改边权重的情况,需要调用以下方法维护cpsum的数量。
public void manageCouple(long id1, long id2, long oldcp1, long oldcp2) {
MyPerson person1 = (MyPerson) getPerson((int) id1);
MyPerson person2 = (MyPerson) getPerson((int) id2);
long newcp1 = person1.getBestAcquaintance();
long newcp2 = person2.getBestAcquaintance();
MyPerson new1 = (newcp1 != Long.MIN_VALUE) ? (MyPerson) getPerson((int) newcp1) : null;
MyPerson new2 = (newcp2 != Long.MIN_VALUE) ? (MyPerson) getPerson((int) newcp2) : null;
MyPerson old1 = (oldcp1 != Long.MIN_VALUE) ? (MyPerson) getPerson((int) oldcp1) : null;
MyPerson old2 = (oldcp2 != Long.MIN_VALUE) ? (MyPerson) getPerson((int) oldcp2) : null;
if (oldcp1 != newcp1 || oldcp2 != newcp2) {
if (newcp1 == id2 && newcp2 == id1) {
cpsum++;
}
if (oldcp1 == id2 && oldcp2 == id1) {
cpsum--;
}
if (newcp1 != oldcp1) {
if (newcp1 == id2) {
if (old1 != null && old1.getBestAcquaintance() == id1) {
cpsum--;
}
} else {
if (new1 != null && new1.getBestAcquaintance() == id1) {
cpsum++;
}
}
}
if (newcp2 != oldcp2) {
if (newcp2 == id1) {
if (old2 != null && old2.getBestAcquaintance() == id2) {
cpsum--;
}
} else {
if (new2 != null && new2.getBestAcquaintance() == id2) {
cpsum++;
}
}
}
}
}
维护valuesum:在进行add person,delete person,modify relation都需要维护
public void manageValue(Person person, int value, int opcode) {
//opcode == 0 add person; opcode == 1 delete person;
//opcode == 2 modify relation(add or change, use value);
switch (opcode) {
case 0:
MyPerson myperson = (MyPerson) person;
HashMap<Integer, Person> acqarray = myperson.getAcquaintance();
for (Person person1 : acqarray.values()) {
if (people.containsKey(person1.getId())) {
valuesum += 2 * person.queryValue(person1);
}
}
break;
case 1:
MyPerson myperson1 = (MyPerson) person;
HashMap<Integer, Person> acqarray1 = myperson1.getAcquaintance();
for (Person person1 : acqarray1.values()) {
if (people.containsKey(person1.getId())) {
valuesum -= 2 * person.queryValue(person1);
}
}
break;
case 2:
valuesum += 2 * value;
break;
default:
break;
}
}
其他维护:如agesum,agemean,agevar实现较为简单,不过多介绍。
性能问题及其修复情况
本单元3次作业皆满分,没有出现性能问题,因此也无修复问题。唯一比较惊悚的一次是第二次作业强测时,有一个一万条指令的数据点在评测机上跑了8s多(吓出一身冷汗)。
对规格与实现分离思想的理解:规格与实现分离是由规格和实现的不同要求决定的。规格的书写要求的是严谨、易读,所以在书写规格时要尽量使用简单的数据结构和最简单的算法(即简单地暴力解决问题),只要严谨地向程序员说明我们需要实现的功能即可。而实现则不仅要考虑程序的正确性,还要考虑程序能否高效地运行。这之间的鸿沟就需要我们去跨越,我们首先要理解JML规格中方法的具体作用,前提条件,限制条件等内容,然后再从一个程序员的角度分析应该采用何种数据结构去管理数据,应该使用何种算法去实现这样一个功能。这个过程也正是规格与实现相分离的过程。
Junit测试方法总结
对照对应方法的JML描述,一条一条地用assert判断其是否已经实现对应功能。个人感觉有点像搭小型评测机,从生成随机数据到利用assert验证方法正确性,一步一步实现即可。
总结起来:
-
需要检查各个ensure的正确性
-
需要检查pure,not_assigned是否满足
-
需要构造足够强度的数据,可以利用参数化测试多组,采用不同的样例类型测试一次保证数据的强度
学习体会
-
jml
使用的时候可以很严格的保证实现不会出错,但是实际在编写的时候,往往算法规格分离。 -
JML规格的编写思路基本上也是我们在编写某个方法时需要考虑的逻辑。对于某个方法,我们当然也需要考虑这个方法的副作用、前后条件等等方面的问题。
-
从根据JML规格实现代码的角度来看,我们需要了解规格管理了哪些数据,对哪些数据进行了操作,其本质作用是什么。在了解这些内容之后,我们需要跳脱规格,择优选择数据结构和算法,然后实现规格要求的功能。