BUAA OO Unit3 单元总结
- 分析本单元的测试过程
- 谈谈你对黑箱测试、白箱测试的理解
- 对单元测试、功能测试、集成测试、压力测试、回归测试的理解
- 数据构造有何策略
- 梳理本单元的架构设计,分析自己的图模型构建和维护策略
- 分析作业中出现的性能问题及其修复情况,谈谈自己对规格与实现分离的理解
- 本单元中同学们实现了Junit测试方法,总结分析如何利用规格信息来更好的设计实现Junit测试,以及Junit测试检验代码实现与规格的一致性的效果
- 本单元学习体会
一、测试过程
黑箱测试
是功能测试,主要检测软件的每一个功能是否能够正常使用。在测试过程中,将程序看成不能打开的黑盒子,不考虑程序内部结构和特性的基础上通过程序接口进行测试,检查程序功能是否按照设计需求规定能够正常使用。
这个类似于中测和强测,通过投喂数据点来测试程序中是否存在不符合要求的漏洞。但是,有限的数据量无法涵盖所有的分支,有可能会漏掉一些不那么常规的bug。
白箱测试
是结构测试,主要用于检测软件编码过程中的错误。把测试对象看做一个透明的箱子,允许测试人员利用程序内部的逻辑结构及有关信息,设计或选择测试用例,对程序所有逻辑路径进行测试。
我认为这个类似于互测,通过阅读同房间别的同学的代码,有针对性地构造一些数据hack掉对方。
单元测试
对软件中的最小可测试单元进行检查和验证,采用白箱测试。
功能测试
需求分析师根据用户需求编写出功能的用例, 然后由测试工程师编写测试用例, 并逐项进行测试验证, 确保执行结果与预期的结果一致,达到预期功能。
单元测试和功能测试体现在junit的编写中,我们针对每一个方法的JML编写相应的测试数据,保证每个方法能够正确实现。
集成测试
在单元测试的基础上,将所有模块按照设计要求(如根据结构图)组装成为子系统或系统,进行集成测试。
主要体现在数据构造中。测试的是完成了单元测试方法之间组成的系统的正确性。
压力测试
压力测试是模拟实际应用的软硬件环境及用户使用过程的系统负荷,长时间或超大负荷地运行测试软件,来测试被测系统的性能、可靠性、稳定性等。
压力测试一般用于测试那些时间开销比较大的方法。比如qcs
、qtvs
等等。通过大量的数据测试代码的运行时间,如果时间过长,则需要优化算法以达到更高的性能。
回归测试
回归测试是指修改了旧代码后,重新进行测试以确认修改没有引入新的错误或导致其他代码产生错误。自动回归测试将大幅降低系统测试、维护升级等阶段的成本。
de完bug后,需要重新测试来确保修改有效或者不引入新的bug。
数据构造策略
手搓: 在刚写完一个方法时,一般自己先捏几个比较简单的点试一试。比如modifyRelation
方法,搓一个简单的五阶图就可以测试大多数情况,不需要对着几千行的数据去debug。
随机: 当然,在不能确定自己程序中是否还存在问题时,使用随机数据是一个很好的方法。它的好处是,比起手动,可以在循环里测试比较大的数据量,更有可能发现一些意想不到的bug。
这两种构造方式的前提,都建立在完全读懂并理解JML上,也就是测试与要求相符合。比如在第三次作业中,我因为忽略了tagSize
为0的情况寄掉了几个强测点,测试的时候也没有想到这种边界条件。
二、架构设计
这次作业的大体框架、各个类之间的继承关系什么的课程组已经给定,我们只需要在这基础上构建我们自己的类即可。
具体来说,就是在一张关系网络Network
中,节点Person
之间存在着关系。同时,每个Person
有自己的群组Tag
。Person
可以在Tag
之间传递Message
,从而更改自己的属性。
图模型构建
- 并查集: 每一个连通子图通过一个代表元来表示。
ap
的时候他自己就是代表元;ar
的时候将两个并查集的代表元设置为相同的人;mr
删边的时候重建并查集,使用dfs分离两个节点分别对应的连通子图:
int delete(int id1, int id2) {
//存储与id1联通的人
dfs(people.get(id1));
if (visited.contains(id2))
return 0;
//把id1认识的人放到一个并查集里
for (int i: visited) {
parent.put(i, id1);
}
//把剩下的人放到另一个并查集中
dfs(people.get(id2));
for (int j: visited) {
parent.put(j, id2);
}
return 1;
}
- 最短路径: 经典的Dijkstra算法~
- 三元环计数: 参考了洛谷p1989,思路是将无向图转化成有向图来计算,如果边的数目是m,那么复杂度将会是
O(m√m)
。
维护策略
这次作业的强测中,有很多刻意卡时间的数据点。
首先,通过调整图算法来避免一些O(n^2)
甚至是O(n^3)
的方法。
其次,通过将一些数据存储下来。举个栗子,保存tripleCount
来避免大量查询操作浪费时间。这其中,我设置了一些脏位来辅助。比如,ar
和mr
时,设置脏位为一,当一次查询完之后,再将脏位设成零。相同的操作包括valueSum
、bestAcquaintance
。
三、性能分析
性能分析
先忏悔一下!!我写这一单元作业时十分地粗心,导致了非常非常多的小bug,因此也没有花太多的时间去优化性能。第一次作业中没有动态维护导致的一大堆ctle。第二、三次作业都是qtvs
的问题。这个最后也是通过动态维护+脏位解决。(以及一些没仔细审JML导致的除以0错误之类的就不多赘述了)
规格与实现分离
在编写JML规格时,主要关注的是方法的输入、输出、前置条件、后置条件以及可能抛出的异常等方面的描述,应该将注意力集中在描述程序的行为和约束上,而不是关注具体的算法实现。而在具体实现时,我们更多地要去关注如何优化性能和简化代码。所以,规格和具体实现一般是分离的。
具体来看,就是我在自己的类里面大量采用Map数据结构。比如,emojiList
和emojiHeat
两个List
,我使用了一个HashMap<Integer, Integer>
来代替,这样就不会出现JML里写了好多的两个List
一一对应的问题了。然后有时候JML里有很多ensures
,实际上只对应了代码中一个简单操作。就像是deleteColdEmoji
一样,其中
emojiHeat.keySet().removeIf(emojiId -> emojiHeat.get(emojiId) < limit);
这一行代码就使用了长达10行的JML去表述。(而且课程组JML已经写得够简洁了,让我来写只会更长)
所以,JML确实就是一个对于行为的约束,关注点在于输入输出规范上;而实现关注点则是在中间的过程上。
四、Junit测试
这次作业的junit采用随机数据。(因为第一次作业手搓交了7次都没过,哭)
在前两次作业中,我们需要测试的是pure方法,并且,这两个方法与图的构造息息相关。所以,我在每一次ar
操作后都测试了一遍方法的正确性,保证稀疏图和稠密图都被测试过。这其中的坑点就是pure方法不改变对象前后的状态,所以我们需要深拷贝一份相同的数据在调用被测方法后与之对比。
第三次作业,重点不在人与人之间的关系上,所以干脆构造了一个16阶完全图,便于信息的发送接受。这次测试要注意的是超长JML中每一个ensures
都必须测到。容易漏掉的情况就是,保证新messages
里有所有的emoji
都是老messages
里面有的hotEmoji
,且老messages
里所有的hotEmoji
都在新messages
里。以及=>
要注意前0和前1的情况都判断到。
五、学习体会
对比前两个单元,这个单元给我的感觉是,思维量小了,但是严谨性上去了,且数据量大幅提升。(颇有oopre的风采)规格描述可以更完整更精确地表述需求,能够有效避免一些争议。不过就这个单元的要求来看,自然语言可能可以表述的更好,以及……JML的变动也是会给程序编写带来一定困扰的。