本单元的测试过程
黑箱测试和白箱测试
-
黑箱测试,也称功能测试、数据驱动测试或基于规格说明的测试。测试者只知道程序的输入、输出和系统的功能,这是从使用者的角度针对软件的接口、功能及外部结构进行的测试,不考虑程序内部实现逻辑。
-
白箱测试,也称结构测试、逻辑驱动测试或基于程序本身的测试,测试程序内部结构或运行。在白箱测试时,从程序设计语言的角度来设计测试样例。测试者输入数据并验证数据在程序中的流动路径,并确定适当的输出,类似测试电路中的节点。
对单元测试、功能测试、集成测试、压力测试、回归测试的理解
-
单元测试是针对软件系统中最小的可测试单元(通常是函数、方法或类)进行的测试。它的目的是验证单元的行为是否符合预期,以确保各个单元在独立测试时能够正常工作。单元测试通常由开发人员编写,并可以在开发过程中频繁运行,以发现并修复代码中的问题。
-
功能测试是对软件系统的各个功能模块进行测试,以验证其是否按照需求规格说明书或用户需求的要求正常工作。功能测试关注整个系统的功能和行为,以确保它能够按照预期执行各项任务和操作。功能测试通常由测试团队或专业测试人员执行。
-
集成测试是将多个独立的软件组件或模块组合在一起进行测试,以验证它们在集成后是否协同工作。集成测试的目标是检测组件之间的接口问题、数据传递问题、功能交互问题等。它有助于发现不同组件集成时可能出现的错误和故障,并确保整个系统的稳定性和一致性。
-
压力测试是通过模拟实际使用情况下的负载和压力条件,对软件系统进行测试。其目的是评估系统在高负载情况下的性能、稳定性和鲁棒性。压力测试通常会将系统推向其极限,以检测性能瓶颈、资源耗尽、内存泄漏等问题。
-
回归测试是在对软件系统进行更改或修复后执行的测试,以确保已经修复的问题没有引入新的错误,并且旧有的功能没有受到影响。回归测试旨在验证软件的稳定性和一致性,以确保在修改过程中未引入新的缺陷或导致现有功能的退化。
数据构造的策略
本单元的数据构造策略是根据JML,对于每个方法的行为进行数据的构造。
数据构造的要点在于保证方法的正确性,对于给出了JML这样行为逻辑清楚的代码,并不需要太多的测试,只需要保证完成了JML的要求即可。
主要的测试策略是,根据JML相应的限制,从ensure,require
等关键字入手,从规格要求的限制上入手,对代码进行相应的测试。
尽管完成JML即可保证代码的正确性,但是意识到“规格并不等于实际实现”,性能问题也是有可能出现的,需要对代码进行一定的压力测试。我简单在单元测试中构造了添加了数万人的超大联通分量情景,对代码的鲁棒性和临界表现进行了一定的测试。
架构设计
基本的架构设计已经由JML给出,没有进行太多的个人延申。为了保证性能、代码风格检查,引入了一个方法类和一个并查集类。
-
方法类是向代码风格检查的妥协,破坏了代码的面向对象风格;建议课程组可以对规格做出一定的优化,不然放松代码风格限制。现在的规格本身就是和代码风格检查相违背的,不是好的规格。
-
并查集类是对性能做出的优化,具体实现和往年博客类似,有相应的合并和优化策略
UML类图为:
图模型建构
图的基本元素是节点和边,节点自然的就是一个个对象,而边的建立依托于价值value的实现,使用hashMap
存储了相应的邻接节点和相应的边值,建立了对应的映射。
同时,图的关系可能是多元的,用Tag等方式呈现了更复杂的网络结构,实现了多重的网络。
图模型维护
维护的难点在于删,保证了合适的删除:涉及双方都要删除,维护的部分要减去对应的值,做好这些后维护就较为简便。
if (newValue > 0) {
MyPerson.SetNewValue(person1, person2, newValue);
NetworkMethods.ModifyTagValue(person1, person2, oldValue, newValue, this.personInTags.get(id1));
} else {
this.tripleCount -= person1.GetSharedAcquaintanceNum(person2);
NetworkMethods.ModifyTagValue(person1, person2, oldValue, 0, this.personInTags.get(id1));
this.blockCount += NetworkMethods.DeleteRelation(person1, person2, this.disJointSet, this.persons);
}
this.coupleCount = NetworkMethods.ResetCoupleNum(id1, id2, bestIdBefore1, bestIdBefore2, this.persons, this.coupleCount);
使用动态并查集重建,保证了每次删边后的正确性,同时还会遍历Tag,维护相应的ValueSum值。
性能
性能问题
对性能优化的核心在于少做无用功。对于重复的工作都要拆分到平时进行维护。也许会占用内存和少查询情况下的性能,但是会显著提高在高查询场景下的性能。
-
连通块:使用并查集维护,通过按秩合并和路径压缩提升效率。删边时,采用连通块重建的方式,DFS删除节点所在的连通块进行重建。
-
三元组:在关系变化的时候,进行动态维护。
-
最短路径,使用BFS方法。图是无权图,使用Dijkstra算法可能反而更慢。
-
BestAccquaince,采用动态维护的方法,在MyNetwork中维护HashMap: bestIds,在关系变动时,记录下双方此时的BestId。
-
ValueSum,可以采用动态维护的方法。在关系变动的时候,对相关的Tag的ValueSum进行维护
规格与实现分离的抱怨
规格与实现的分离在于,规格中给出的方法是最为质朴的数组,但是落后的数据结构会代码落后的性能表现,在实际实现中往往会使用哈希表等高级数据结构。
我不认为说,在实际开发中,规格与实现是分离的是合理的解释。如果对代码性能有要求,那么在契约式编程中就应该提出来,而不是让程序员面对之传达功能翻译的代码进行自己优化。如果后续的功能规格改变了,如要求对容器的有序输出,那么原先的哈希表实现就要修改,可能是对代码底层的较大修改。这是程序员的问题吗,我不觉得是。
当然,这只是在象牙塔中的抱怨,实际的开发中肯定没有清晰的规格,和产品经理的扯皮才是常态。
JUnit
单元测试JUnit测试是偏向于细致测试的,可以对每一个方法进行单元测试,用正确的方法组合保证整体方法的正确性。
但是JUnit的矛盾点在于测试的效率问题:短方法不需要测,长方法测起来麻烦。在之前的课程中,由于代码的逻辑并不复杂,我比较抗拒JUnit,认为在效率上不如瞪眼法来的快。
但是在给出规格后,原先Junit过于细碎的测试的局面改变了。根据规格挨个检查代码的正确性,我认为这是契约式编程的灵魂所在。具体而言,我会按照规格检查assignable的对象、ensures的内容、具体的exception、result的值。
通过对这些明确的规格,可以很好地完成对代码的测试,JUnit和JML是可以达到1+1>2的效果的。
学习体会
本单元还算顺利,没有花很多时间就结束了。
要说感想的发布,那就是感觉对JML的失望。刚开始时感觉JML还是很方便的,给出了详细的功能规格定义。对于契约式编程而言,如果期望对程序的性能有一定的要求,那么最好将性能也提前告知,可以是方法的复杂度,也可以是圈复杂度等其他衡量方式,而不应该是在高并发不确定的评测机去对一万条指令的总运行时间做出要求,并没有很好地体现整体的性能。此外,本单元的名字是规格设计与分析,并不是算法设计与分析,在规格单元考察算法,我觉得是没有必要的。
撇去JML对性能的忽视不谈,课程组反复修改JML也体现了JML的不成熟。包括体验并不好的JUnit,本单元也许教会了我部分JML的相关知识,但是我并没有体会到契约式编程的魅力:反复修改、不尽完善的规格没有起到良好的训练效果。
我本人也在做第二次作业指出过指导书中样例的错误,体验很难说好。