301
社区成员
发帖
与我相关
我的任务
分享本单元以维护简单的社交关系网络为背景,训练JML的使用和契约式编程思想的理解。此外,在具体实现时还穿插了一些图论与算法的内容。
黑箱测试与白箱测试:
黑箱测试也称功能测试、数据驱动测试或基于规格说明的测试。它将被测程序看作一个打不开的黑箱,并不关心黑箱里面的内容,只关心程序要实现的功能。因此,黑箱测试只关心软件的输入数据和输出结果,测试是否正确,是从使用者的角度对软件的接口、功能进行测试。
白箱测试也称结构测试或逻辑驱动测试,是在知道程序内部的工作过程以后,按照程序内部的结构测试程序。测试者针对程序的实现构造测试用例,检测程序内部动作是否按照规格进行,检验程序中的每条通路是否都能按预定要求正确工作。本单元要求的JUnit测试体现了白箱测试的思想。
功能测试:
即测试软件的功能是否符合需求,关注整个系统的行为,通常采用黑盒测试方法,一般由测试人员独立执行。
单元测试:
单元测试指对软件中的最小可测试单元进行检查和验证。这个“单元”通常是某个方法,在大型软件中也可以是某个类、功能模块或者子系统。本单元中编写的JUnit就是单元测试。
集成测试:
与单元测试对应,测试与模块接口有关的问题(如模块间的调用)。目的是测试通过了单元测试的各模块能否作为一个程序整体良好运行。集成测试中应当避免一次性的集成,而采用增量集成。
压力测试:
在高负载的情况下进行测试,用边界输入(边界数据、边界输入量)、多用户或者高并发等情况测试,测试程序在极限情况下是否仍能稳定运行及性能。本单元中主要体现为通过大量的顶点、边的大量增删查改操作,检验程序性能。
回归测试:
在修改代码之后,重新测试先前的测试用例以保证修改没有引入Bug。
数据构造策略:
由于这三次作业中,后一次作业基本都不会改变前一次作业实现的指令,因此我每次主要对本次作业新增的指令构造数据测试。针对每次作业新增的指令,先生成基本的图(包括没有顶点的图,少量和大量顶点的无边图、稀疏图、稠密图、完全图),再生成少量的加顶点、增删改边的指令,中间穿插大量的查询指令,前者与后者的比例为3:7左右。同时注意有意生成一定数量的异常,但不必太多,因为感觉异常只要照搬JML给的抛出条件写,应该不会出错。
此外,压力测试数据的构造也是必不可少的。我对每个实现复杂度超过O(1)的方法手捏了一万甚至几万行有特点的测试数据进行测试,以验证其不会超时。一般来讲,本地CPU时间在1s以内时,才可以保证评测的CPU不会超时。较好的实现可以实现在压力数据下本地CPU时间仍在500ms以内。
本单元的基本架构按照JML实现即可,不必自行做很多设计。只有在使用并查集时,为了使类的职责更清晰、专一,降低代码的耦合度,我没有将并查集的操作写在network中,而是封装进了一个并查集类DisjointSet中,这样,并查集的并、查、重建、路径压缩、按秩合并,就可以都由它自己完成。
图模型构建:
Network:整张社交关系网络,用HashMap<Integer, Person>存储各个人(节点),用HashMap<Integer, HashSet<Integer>>存储与每个人邻接的节点集合(各条边)。
Person:一个节点,其中用HashMap<Integer, Integer>存储各邻接顶点,及其边的权值;用HashMap<Integer, Tag>存储所拥有的各Tag。
Tag:本质上可以把它看成一个子图,用HashSet<Person>存储该子图中包含的节点。
维护策略:
在本单元三次作业的编写中,逐渐认识到了这样一个维护的原则:在本单元10000条数据的限制下,O(n^2)的时间复杂度必须优化,O(nlogn)的时间复杂度勉强可以接受, 而O(n)的时间复杂度不会出问题;能优化则优化。因此,对于本单元中复杂度O(n^2)甚至更高的方法,我都大力使用(有些复杂但效果显著的)动态维护方法优化,如qts、qtvs。此外,对于一些本身实现为O(n)的方法,如qtav,也将它优化为O(1)的复杂度,实现也并不复杂,以尽可能减少CPU时间。
如上所述,由于采用了如上的动态维护,本单元三次作业在强测与互测中均未出现性能问题。
规格与实现并不是等同的。规格的作用是说明一个方法的功能,即对于给定的输入,应当输出什么或产生什么异常,仅仅是从功能上定义了这个方法,并没有定义具体的实现路径。如果把编写代码看成一段行程的话,规格就是告诉了你出发地、目的地,而选择什么样的路径以达到路径最短或者最为省钱,则完全取决于你自己,取决于自己的设计、选择。因此,在读完规格,弄明白方法要实现的功能以后,就应该仔细地思考实现的方案,以确定一个实现规格的最优解。一个规格有多种不同的实现,两者之间除了功能相同外并没有必然的联系,而我们在本单元中要做的,就是选择一个实现规格的最优解,以在完成功能的同时,尽可能提升实现的效率。
JUnit是一个强大的单元测试方法,可以用它对一个方法的正确性进行全面地测试。规格正好给我们列出了需要检查的各个方面,因此利用规格和JUnit结合进行测试十分方便,且准确性很高。具体讲,我们要对规格中的assignable(或pure)、ensure及signal语句进行检查,检查方法是否修改assignable没有允许的修改、违反ensure规定的行为、没有抛出signal规定的异常。根据规格逐项检查代码极大地体现了契约式编程的优越性,可以准确、高效完成对代码的测试,有着出人意料的效果。
本单元让我们学习了根据JML规格编写代码,学习契约式编程的思想。学习之后我认为,规格(不仅限于JML)是一种十分高效的软件开发方式,它将设计与实现分离,且使彼此都十分清晰。契约式编程,使软件的质量得到明显提升,也大幅度降低了设计、开发、测试的难度。同时,使用统一、严谨的形式化表述,也降低了不同环节沟通的成本。以后的工作学习中,使用的具体语言或许不是JML,但契约式编程作为一种软件开发的思想,一定是使人受益终身的。
此外,本单元作业还使我认识到程序复杂度和压力测试的重要性。强测的压力数据使我们不得不尽力搞好每一个方法的优化,考虑各种极端情况,并在本地做好压力测试。强测CPU时间与本地CPU时间有差异,因此,我们可以比对Hw9中的强测CPU时间与本地CPU时间,找出大概的一个倍数关系,以此为参考,得出“O(n^2)及以上必须优化”和“能优化则优化”的原则,并据此评判Hw10、Hw11实现的性能。这样做,才可以保证强测没有CPU超时。压力测试是我在本单元学到的一个新的方法。