OOUNIT3_课程总结

目录

1 测试

1.1 黑箱测试和白箱测试

1.2 测试方案

1.3 数据生成逻辑

2.社交网络的构建与图论

2.1 问题抽象

2.2 图论问题解决

2.3 动态维护 

3 性能问题与修复

4 规格与实现

5 Junit

5.1 基于规格编写评测文件

5.2 基于规格测验的效果

6 心得体会


1 测试

1.1 黑箱测试和白箱测试
  • 黑箱测试也称功能测试、数据驱动测试或基于规格说明的测试。测试者只知道程序的输入、输出和系统的功能,这是从使用者的角度针对软件的接口、功能及外部结构进行的测试,不考虑程序内部实现逻辑。
  • 白箱测试也称结构测试、逻辑驱动测试或基于程序本身的测试,测试程序内部结构或运行。在白箱测试时,从程序设计语言的角度来设计测试样例。测试者输入数据并验证数据在程序中的流动路径,并确定适当的输出,类似测试电路中的节点。
1.2 测试方案
  • 单元测试是指对软件中的最小可测试单元进行检查和验证。

单元测试一般分为人工静态检查和动态执行追踪。人工静态检测就是测试人员对每个单元功能块逐行阅读,检测代码是否正确完成功能。体现在本次作业中,我们需要在完成某个方法后,检测方法是否严格按照JML实现。

动态执行追踪是将代码运行后,观察代码单元在执行过程的行为逻辑的正确性,一般采用Junit。

  • 功能测试测试软件的功能是否符合需求,通常采用黑盒测试方法。

功能测试是观测在给出指定输入后,只需要检测软件能否完成相应功能,这部分忽略了内部代码细节,测试人员不需要知晓内部代码,只关注功能是否实现。

  • 集成测试被定义为一种测试类型,其中软件的不同模块被集成并作为一个整体进行测试。

在软件中我们需要将有关联的各个模块集合起来进行测试,观察各个模块协作完成情况。集成测试用例与其他测试用例的不同之处在于,它主要关注模块之间的接口和数据/信息流。在此优先考虑集成链接,而不是已经测试的单元功能。

  • 压力测试是指持破坏性目的对代码的抗压能力进行测试。

压力测试不仅仅是采用边缘数据点进行测试,有时需要使用一些不太符合规范的数据点,观察代码是否仍可以正确运行,不会崩溃。这种测试有可能会发生对整个代码系统的破坏,导致软件崩溃。

  • 回归测试是一个系统的质量控制过程,用于验证最近对软件的更改或更新是否无意中引入了新错误或对以前的功能方面产生了负面影响

在迭代过程中,新加入的功能可能会对之前已有功能产生影响,这时我们需要构造相应测试样例检测之前模块的正确性。在我们的作业中,我们需要在每次迭代后,使用之前的测试样例进行测试,保证旧功能不会因为新功能产生错误或性能问题。

1.3 数据生成逻辑

基本功能测试。第一步的构造测试样例是用来检测这部分代码的功能是否正确完成,仅仅测试代码完成度。

随机数据构造。第二步将会使用数据生成器按照一定的生成逻辑(两种方式稠密图和稀疏图),大量随机生成数据,进行黑箱测试,并通过海量数据来保证程序的正确性。

特殊情况考虑。第三步会人工构造一些边缘数据点,如10000条数据点,且为完全图(针对BFS),或是退化二叉树的深层次图(针对DFS)。这些样例验证自己程序在一些特殊情况下不会出错。

极压力测试。第四步为了保证性能,将会数据生成器将会生成不符合规范的数据,如指令条数达到30000条,图的构造将会更加复杂,这些情况来验证程序性能高低。

2.社交网络的构建与图论

2.1 问题抽象

本单元要求维护一个社交网络,我们将每一个人看作一个节点,每个人直接的联系作为边,这样问题就抽象为一个图论问题。以下是时间复杂度较高的几个方法:

  1. 第一个是isCricle(),这个方法的规格让我们查询,一个人是否可以不断通过熟人找到另一个人,这个问题可以抽象为在一个图中,两个节点是否是可达的。
  2. 第二个是queryBlockSum(),虽然JML语言描述的很复杂,其实就是求极大连通子图的个数。
  3. queryTripleSum()查询三个人互相存在联系的三元组个数,这个问题抽象为求一个图中三角形的个数。
  4. queryShortestPath(),顾名思义求两个点之间的最短距离,但是这里问题简化,每个边的长度都是1.
  5. queryTagValueSum(),求一个分组中各个成员之间的亲密度总和,其实是在求导出子图边长度的和。
2.2 图论问题解决

首先我们需要解决的是isCircle()判断节点的可达问题,这个采用并查集较为简单,否则每次查询带来的性能开销过于庞大。对于并查集的建立和维护主要涉及三个函数:

private HashMap<Integer,HashMap<Integer,MyPerson>> groups;
public void addPerson(MyPerson person) {
        
    }

public void addRelation(MyPerson person1,MyPerson person2) {
        
    }

public void delRelation(MyPerson person1,MyPerson person2) {
    person.search(person1,hashMap);
    if (hashMap.contains(person1)) 
           return;
    else 
        split groups(person2.getId()) from groups(person1.getId())
 }

并查集实际上是将一个图分为若干极大连通子图,初始时每个点都是只包含自己的极大连通子图(实现在addPerson)。当建立联系时,将较小的子图并入较大的子图(实现在addRelation)。

这里比较困难的是删除关系时,带来并查集维护的问题。传统并查集并不支持“删边“操作,我们需要设计算法完成这一过程,并且尽可能的减少时间复杂度。我们删边的过程有可能会让person1和person2之间变的不可达,如果,这样我们之前建立的并查集就会失效。一种解决思路就是,让person2采用DFS/BFS搜索person1(找到就返回),如果仍可以搜索到,那么不需要任何操作;否则,在搜索过程中遍历到的每一个节点,都是以person2为根节点的新分支。这样删边的复杂度在最坏情况下是nlogn(线状稀疏图),较好情况下近似于O(1)(近似完全图)。

同时由于并查集的建立,第二个问题只需要返回并查集维护的hashMap的size即可。

求解两个点之间的最短路径,每条边长度相同的情况下,采用BFS算法,最先搜索到这个点时所经过的步骤就是最短路径长度。

2.3 动态维护 

动态维护一般出现在单次查询时间过长而没有较好的优化算法的情况下采用的效率,如求解三元组的个数,我们不可能遍历去求解,否则将会带来巨大的性能开销(O(n^{3}))。对于本次作业,时间复杂度要求的单个方法不能超过O(nlogn).对应的总计算过程次数,经过本人实测,大约极限在5000万到1亿之间,若超过,发生CTLE错误。动态维护会使得维护过程的复杂度增加,但是在这个问题上,一般维护带来的开销不会超过O(n),而查询就是O(1)复杂度,所以采用动态维护就是优化最坏情况,即使可能会降低大多数正常情况下的效率(一般课程组会采用极端数据,优化在强测上还是有意义的)。

我们需要考虑什么时候会产生三角形,当存在AC,BC两条边的时候,在增加AB边时就会产生三角形。所以,每次增加边的时候,我们遍历这两个点的公共邻接点,三角形的个数增加公共节点数。删除边同样采用这个过程。动态维护让算法复杂度大大降低,维护为O(n),查询为O(1).

第二个需要动态维护的是bestAcquaintance,求一个人与它关系值最高的联系人。这里我们常常使用大顶堆来保存最大的元素(数据结构为java中的优先级队列),或者是红黑树(数据结构为java中的treeSet),这里都会在维护上耗费O(logn).

还有一个必须动态维护的是ValueSum的求解,这个问题可以抽象为需要求出一个导出子图的所有边长度的和。如果每次计算采用遍历求解,复杂度极大,所以需要监视子图中当边增加或减少时,修改ValueSum。这里我们需要记录每个点在那个tag中,由于tag的id不唯一,我们可以采用类OS的mkenvid()的方法,给每个tag生成唯一的id,这样便于我们全局维护。

除了常规的动态维护,设置脏位实现简单,性能不低,是一种性价比较高的做法。对于一些数据我们求解后记录当前值,直到发生变动操作后(mr指令),再次重新计算。

3 性能问题与修复

在三次作业中,有时看似动态维护带来性能提高,但它只是把较坏的情况下的情况变好了,但有时可能会因为较好情况下维护开销过大带来性能问题。课程组似乎发现大多数同学采用并查集,并对增删边维护。所以反其道而行,采用特殊数据点,攻击维护过程(不断的modifyRelation).这时如果其他方法实现的不是非常好,就会在这个地方出现性能爆炸。

实际上,课程组特别喜欢压力测试和单点爆破,也即每个测试点着重测试一个方法,如hw2中strong10的几乎全是qtvs,hw2的strong5在不断询问最短路径。这样,我们的选择就是,不要将许多维护的开销放在一个函数上,我们要将复杂度均摊起来。

另外,也可以分析出来大多社交网络图的方法复杂度本质上依赖于边的多少,也即是稠密度。当边较多时,各个方法的复杂度都会上升(课程组和互测也喜欢稠密图性能攻击)。所以,我们可以秉持优化大概率情况的原则,将算法对稠密图情况下做出一定的优化。比如搜索时采用广度优先比深度优先更有优势。

4 规格与实现

规格是一种契约化编程,它避免了自然语言描述存在二义性的问题,使得编程遵循一种规范化的过程。规格是一个画靶的过程,在编写规格时,我们往往会忽略问题的具体细节,而是将问题抽象出来,用JML语言描述。就像射靶子,我们在规则允许的条件下无论用何种姿势射击,只要保证射中靶子就完成了任务。

所以规格和实现是分离。在设计架构时,我们就需要给每个类的属性和方法编写规格,弄清楚我需要实现什么,需要在实现过程中遵守那些规则。而实现过程就是将这个方法规格要求的目标,用一种较为合理且不打破规格的方法去实现,这一过程要求我们关注问题的具体细节,实现最为合适的方法。

规格和实现的步骤不能颠倒融合。当我们先实现了一个方法,这时我们就有一种先射箭后画靶的谬误。在书写规格时,就有可能会受到实现中细节的限制,使得规格不能很好的描述这个问题。就比如,最短路径问题。在正确的规格中,我们只需要要求,存在这样一条路径,使得两点之间距离最短。我们不需要关注是否有这样的路径,怎么去求,怎么高效的具体细节问题。而在方法实现中,我们就需要考虑这些问题,就比如采用Dijistra或BFS搜索。如果我们已经使用了BFS去实现这个方法,在描述上就缺少了问题的抽象过程。这也是,规格和实现是两个不同的过程,需要将它们分开完成的原因。

5 Junit

5.1 基于规格编写评测文件

本单元作业和前两个单元不同的是需要编写test文件,对课程组给出的代码进行测试。测试代码的编写主要分为两部分:数据生成和基于规格进行评测。

先说以下评测逻辑,规格一般有前置条件requires.在评测前,我们需要检验数据是否满足前置条件,根据条件的满足性分发给不同的测试代码部分。assignable限制我们方法的副作用,这部分是评测的最主要的难点,我们需要对类中一切不允许赋值的属性进行检测方法执行前后是否存在差异。一般是在方法调用前保留这部分信息(深拷贝),方法调用后,采用equals或“==”比较前后是否相同。

最后一步是检查后置条件ensure的满足。一般上,需要我们将方法调用后,提取被赋值或修改的属性以及方法调用返回值。我们需要一步一步严格按照JML规格的限制,进行条件判断满足。对于返回值的计算,一般需要我们在评测方法内实现一个绝对正确的方法计算实现,用这个标准的计算结果和待评测方法返回结果比较(一般严格按照JML描述的计算过程)。

数据生成需要我们阅读规格的一些信息,如前置条件,来生成一些对规格覆盖率较高的数据点。虽然课程组要求我们不需要很强的数据点来判断错误,但在这句话并不意味着我们可以随便生成数据。如hw10需要我们生成尽量没有联系的特殊图,hw11要求我们生成含有四种message的数据点。

5.2 基于规格测验的效果

Junit一般是细致的测评某一个方法是否正确的实现了它的功能。而评测机都是黑箱测试,这就可能回导致某一个方法的副作用或是行为逻辑不对,但是由于返回值正确而测试通过。规格测试,测验了我们方法该做的完成了没有,是否发生不规格的行为,这种测试对方法实现了逻辑的测验更加严格。由于我们知晓方法的具体行为,我们可以有预见地生成对应数据,做出相应的JML测评,方法的测评逻辑也是非常容易书写,并且测评的覆盖率可观。同时,这种测试的可见性让我们极大提高了方法实现的正确性和可靠性。

但是,这种创新也带来一些问题,具体体现在公测上。由于测试代码不可见,我们不知道可能有什么牛鬼蛇神的错误代码,而我们测评一般是基于正常实现来测评的,导致需要专门消耗一定的次数来推测出这个数据点具体是在测什么,我们需要着重注意哪些方面(有的代码在一定条件下会更改一些奇怪的属性)。所以,我们基于规格测验是来提高自己的测验意识,而不是通过这个来难为大家,我希望测试数据点可以提示测试点需要我们完成哪些测试。OS课程的测试点虽然不公布,但是会公布这个测试点具体在测哪个功能,利于我们针对性的更改代码,而不是像一个无头苍蝇。

6 心得体会

OO的第三单元围绕JML展开,每一个人戴着镣铐跳舞,方法的实现需要满足JML的限制。由于作业的方法实现过程已经给出,大大降低了程序编写的难度相比于第二个单元的基本无引导。虽然我们只需要按照JML的描述完成方法构造,但是JML给出或暗示的方法大多都复杂度较高,所以说我们还是需要思考一些算法来避免强测出错(本质上这单元是图论)。

由于上述提到的种种原因,Junit的编写和debug需要的时间远远大于真正功能代码编写,确实让人感觉不适。或许是这单元真正的训练目标是基于JML展开测试?不过经过第一次的苦苦提交,后两次的测试程序编写就变得较为简单,如生成有针对且覆盖率达标的数据,影子测试保证not_assignable的限制。

不论如何,这单元的体会还是很特殊的,我既掌握了JML的低级语法,也了解了一些图论算法,同时了解一些测试程序的书写方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值