OO UNIT3 单元总结
单元测试过程
黑箱测试和白箱测试
- 黑箱测试:黑箱测试又称为功能性测试,该测试将程序视作一个不可拆分的黑盒;不了解程序的如何实现功能,如何处理数据,仅仅依据设计要求和规格以及输入和输出的数据来对程序进行功能性测试。黑箱测试侧重于测试程序的“功能性需求”。我们平时使用评测机来进行对拍测试,检测结果的正确性就是一种黑箱测试。
- 白箱测试:白箱测试又称为结构测试、透明盒测试。白箱测试要求测试人员了解程序结构与程序实现的过程,按照程序内部的逻辑来进行测试。基于这些理解,来构造数据进行测试。例如我们互测环节的对程序覆盖率,代码逻辑的测试,以及os课程中的测试都可以视作一种白箱测试。
上述两种测试方法在我个人的程序测试过程中都有所用到。对于JML规格要求的各个方法和类,我主要采用了黑箱测试的方式,因为函数数量繁多,白箱测试难度相当之大;而且每个人实现JML的逻辑也不尽相同,无法进行对拍。而对于OKTest方法,我主要采取了白盒测试的方法,OKTest的逻辑较为简单,完成路径遍历较为容易,而且数据生成器难以生成OKTest的数据,因此采用白箱测试。
单元测试、功能测试、集成测试、压力测试、回归测试
-
单元测试:单元测试是对软件中的最小可测单元进行测试,例如c语言中的函数、Java中的类和方法等。单元测试可以测试一个最小单元是否可以正确运行,确保这些单元的正确性,可靠性和性能。进行单元测试可以准确的定位错误出现的位置。
-
功能测试:测试软件是否符合文档的规格要求,上述的黑箱测试和白箱测试都属于功能测试。
-
集成测试:集成测试是将已经用过单元测试的模块结合在一起进行测试,来测试单元在集成时是否能够正常运行。集成测试目的是为了检测组件间的接口和交互是否正确,确保在集成后能够正常运行。
-
压力测试:压力测试是为了评测程序在高负载情况下稳定性和性能的测试,常常通过极端数据(复杂的数据,高并发)等,来测试程序在高负载时候的稳定性和性能,进行优化性能,寻找漏洞。
-
回归测试:回归测试是在程序修改和增加了新功能后,重新对先前程序进行测试,确保没有出现新的错误或者影响原有性能。回归测试很多时候可以利用之前使用的数据,避免重复构造数据。
在本单元中,上述测试基本都有所涉及,例如第三次作业中我们就需要对qlm指令进行压力测试,每次迭代后,也要对先前写好的程序进行回归测试,确保没有引入新的bug。
测试工具和数据构造策略
- 对大多数的单元测试,主要利用数据生成器来生成数据,利用数据生成器可以生成大量随机的数据。使用对拍机和标准程序或者其他同学的程序进行对拍。
- 对于OKTest的测试,数据生成器的编写较为困难,因此需要人工捏造数据,捏造的数据需要覆盖每一个ensure点;对于压力测试,特别是qml指令,用数据生成器很难生成稠密图和稀疏图的样例,这样一来就很难检测在稠密图和稀疏图情况下的性能,需要手工编写数据,
第三次作业就是因为这一点导致超时(本地运行时间正常)。
框架设计
三次作业的框架严格按照JML来完成。存储数据的容器主要采用了arraylist
和 HashMap
来存储数据。在完成规格要求外,我此外写了一个静态类Algorithm
,用来实现一些算法,例如dijkstra
, DFS
,OKTest
等,避免。Netwaork
类过长
第一次作业
本次作业中,需要动态维护的只有qci和qbs指令。此外,我没有构建图,只存储了每一个点的临接点。对于两点是否联通,我采用了动态规划的算法,创建一个hashMap
存储一个点和他所在分支的编号。在加入一个新人时,将其对于成一个新分支,同时将总分支数加一。
people.add(person);
map.put(person, idNumber++);
circleSum++;
当建立连接时,若两个点不连通,将其中一个分支的id赋值成和另一个id相同。(可以通过hashMap
快速得到每一个人所在的分支)。
int blockId1 = map.get(person1);
int blockId2 = map.get(person2);
if (blockId2 != blockId1) {
for (Person person : map.keySet()) {
if (map.get(person) == blockId2) {
map.put(person, blockId1);
}
}
circleSum--;
}
此外,在创建连接时,也需要维护三元环的数量。可以遍历一个点的所有邻接点,查询是否有邻接点和两点相邻。
for (Person person : ((MyPerson) person1).getAcquaintance()) {
if (person.isLinked(person2)) {
tripleSum++;
}
}
完成上述动态维护后,对于qci和qbs指令可以直接相应的值,大大降低的时间复杂度。
第二次作业
第二次作业新增了很多内容,很多指令也可以通过动态维护来优化。
-
query_couple_sum
也可以通过动态规划来实现,由于int数据类型的精度问题,获取方差时不能直接用最简公式。ageSum += person.getAge(); ageSquareSum += person.getAge() * person.getAge();//加点 ageSum -= person.getAge(); ageSquareSum -= person.getAge() * person.getAge();//删点 return (ageSquareSum + people.size() * getAgeMean() * getAgeMean() - 2 * getAgeMean() * ageSum) / people.size();//计算方差
-
本次作业新增了
modify_relation
指令,在特定情况可能会有删除联系的操作,这对动态规划带来了很大的麻烦,当删除两边关系后,两点可能不可达后;实际上,这种情况下,分支的数量增加了,可以通过DFS判断两点是否联通,若不联通,需要为其中新分支赋值。同时,包括两点的三元环也许要减去。for (Person iperson : ((MyPerson) person1).getAcquaintance()) { if (iperson.isLinked(person2)) { tripleSum--; } } if (!Algorithm.myIsCircle(person1, person2, 0)) {//采用DFS判断联通 circleSum++; Algorithm.setBlockId(person1, idNumber++, 0, map); //将所有联通点对于的分支id设置为idNumber }
-
query_couple_sum
指令也能通过动态规划优化,主要是可以获取每一个点的最佳匹配点,在新增和删除邻接点时,需要其进行修改。
第三次作业
第三次作业的主要难点在于query_least_moment
(qlm)指令,该指令的要求时读出指定点的最小加权环,返回权重的和。
可以通过dijkstra
算法或者Floyd
来查询最小环环,但是Floyd
算法复杂度为o(n3)很明显复杂度过高,采用dijkstra
算法可以将算法复杂度压缩到o(n2),我提交的便是这种。但是在强测中,我依旧有两个点超时,原因在于我使用的多个hashMap
会继续多次检索,此外,没有堆优化的dijkstra
算法复杂度依旧过高,会导致超时。最后,我将算法优化为有堆优化的dijkstra
算法(算法复杂度为o(mlogn)),通过了测试。可以直接使用java自带的优先队列来实现堆。实现如下
PriorityQueue<Edge> queue = new PriorityQueue<>();
queue.add(new Edge(person, 0));
while (!queue.isEmpty()) {
Edge edge = queue.poll();
list = map.get(edge.person);
if (list.get(1) == 1) {
continue;
}
list.set(1, 1);
for (Person person1 :((MyPerson) edge.person).getAcquaintance()) {
list1 = map.get(person1);
if (list1.get(0) == -1 || list1.get(0) > list.get(0)
+ person1.queryValue(edge.person)) {
list1.set(0, list.get(0) + person1.queryValue(edge.person));
if (edge.person.equals(person)) {
list1.set(2, person1.getId());
}
else {
list1.set(2, list.get(2));
}
list1.set(3, list.get(3) + 1);
queue.add(new Edge(person1, list1.get(0)));
}
}
}
在查出所有点的最短路径和路径长度后,遍历每一个点就能算出最小环。
int ans = -1;
ArrayList<Person> list = new ArrayList<>(map.keySet());
ArrayList<Integer> list2;
ArrayList<Integer> list1;
for (int i = 0; i < list.size(); i++) {
Person person1 = list.get(i);
list1 = map.get(person1);
for (int j = i + 1; j < list.size(); j++) {
Person person2 = list.get(j);
list2 = map.get(person2);
if (person1.isLinked(person2) && !list1.get(2).
equals(list2.get(2)) && list1.get(3) + list2.get(3) >= 4 && (ans == -1
|| ans > list2.get(0) + list1.get(0) + person1.queryValue(person2))) {
ans = list2.get(0) + list1.get(0) + person1.queryValue(person2);
}
}
}
性能分析与规格与实现分离
在本单元,我们第一次接触到了规格,了解到了JML这么语言并且简单的了解了该门语言。在编写代码时,我们不能仿照JML通过多次遍历循环来完成功能,否者性能会相当只差。在我看来规格与实现分离就是先要通读JML,在心中将其翻译成自然语言,再通过自己理解出来的需求完成代码,实现代码可以有多种多样的算法,也要用算法来提高程序的性能。
再前两次作业中,我采用的动态规划的方法,在测试中并没有出现问题。而在第三次作业中,由于我只采用了普通dijkstra
算法而且对同一个数据有多次索引,导致了有两个点超时。在bug修复中,我优化了数据结构以及采用了堆优化的dijkstra
算法,成功完成了修复。
OKTest
OKTest测试程序的编写可以完全依照规格要求,将JML逐行翻译成代码,对每一个ensure
要求逐行确认,若不满足要求就将返回出现错误的地方。通过这种方式就很容易完成OKTest测试程序的编写。
提出的建议:第二次作业的OKTest有23个返回值,而一个方法被要求在60行内完成,这就不得不写几个方法来完成OKTest程序,无法在一个方法内完成,可以将OKTest检测的范围继续削减。
心得体会
在本单元,我们第一次接触到了规格,粗略学习了JML这门行为接口规格语言。但是JML给我最大的感受就是难读又难写,相较于自然语言,JML确实很严谨但可读性确实很差。
除了JML,在本单元我也学习到了很多算法,例如并查集,Floyd
算法;也复习了之前所学习的DFS
、BFS
和dijkstra
算法;为了优化程序性能,也和同学多次交流,寻找合适的算法。也让我了解到了,数学知识对于程序算法的编写大有帮助。
最后,本单元再一次让我意识到了测试的重要性,相较于之前的章节,本次章节要完成的方法,类都远多于之前的章节,人工检测的难度相当之大,没有评测机对拍机很难完成测试。提高自己编写评测机的能力。