OO第三单元——JML规格单元总结

董鑫-21374113 学生 2024-05-18 18:16:24

本单元的测试过程

黑箱测试与白箱测试

  1. 黑箱测试:

黑箱测试是指,测试人员不需要知道被测试程序的内部实现细节,只需要知道如何使用该程序,以及程序的输入和输出。
在不了解测试程序内部的实现时,设计测试用例,获取测试结果,并分析测试结果与预期结果是否一致。
黑箱测试是站在用户的角度出发,对程序功能的正确性进行检验。

在设计测试用例上,需要通过经验来构造某些边界值或错误情况,有可能无法覆盖程序中的某些特殊的分支条件。

在本单元测试中,编写作业所需的测试,就是一种黑盒测试,
在不知道课程组所给程序的具体实现的情况下,检验所有规格是否正确实现。

  1. 白箱测试:

白箱测试是指,测试人员需要了解被测试程序的内部实现细节,并能够进行详细的测试。
在知道程序内部的实现细节的时候,便可以构造某些边界情况,并对程序的各个分支条件进行测试。
有助于对程序的各个功能分支进行更全面的检验。

但是,在知道了程序的内部实现后,测试本身可能就带有某些偏见,而失去了部分测试的独立性。
此外,如要对程序的各个可能的分支甚至可能的分支组合进行详细的测试,其测试用例的数量可能极为庞大。
另外,白盒测试是按照程序当前的实现为基础构造测试的,
相比于黑盒测试,根据规格、文档,关注于输入输出之间的关系,在程序更改后,可能无法按照预期进行正确的测试。

而互测由于可以获取到同学的代码,因此可以考虑使用白盒测试的方法,
构造某些边界值或是部分关键分支的测试用例,监测程序是否工作正常。

单元测试、功能测试、集成测试、压力测试、回归测试

  1. 单元测试

单元测试是指,对程序的某个最小的可测试单元,如函数,类,进行的测试与验证。

单元测试,用于验证程序的各个功能模块,是否工作正常,是对程序的某个小的部分进行的验证,不对程序的各个功能模块进行组装,
有利于发现程序的某些函数、类的实现是否正常。用于程序早期的开发阶段。

  1. 功能测试

功能测试,是用于测试程序的功能是否符合需求,确保程序按照预期的方式运行。
对程序的各个模块,系统,以及整体进行正确性的测试。

  1. 集成测试

集成测试,是在单元测试之后,将程序的各个功能模块联通起来后,进行模块之间的接口有关的测试,
用于发现程序各模块之间的交互是否正常,各个模块之间传递的参数是否符合规范,来找出程序单元组装之后出现的错误。

  1. 压力测试

压力测试是性能测试的一种,通过大量数据,对程序进行极高负载的测试,用于评估程序在极端条件下的工作情况,
测试程序的健壮性以及错误处理能力。

  1. 回归测试

回归测试是在程序被更改后,使用原本通过的测试,对修改后的程序,进行重新的测试,以此评估程序在修改后,是否引入了新的错误。

以上几种测试,是程序测试的根据不同分类方式下的不同测试方法。白盒测试和黑盒测试,是根据是否知道程序的内部实现细节来分类的。
单元测试和集成测试,是程序不同开发阶段下的测试。功能测试和压力测试分别是对程序的功能和性能下细分的不同测试。
这些测试方案,可以互相交叉,在测试过程中,需要考虑多方面因素,结合不同的测试手段进行详细的测试。

数据构造策略

本单元中,由于是在学习jml规格文档的内容,程序的各个模块之间的交互较为简单。测试以单元测试以及整体测试为主。

在数据的构造方面,由于程序的分支结构较为简单,通常以jml描述的不同behavior和signal的条件作为分支条件即可完成作业,
对于这部分的分支测试,按照不同分支的条件,构造对应数据即可。

而另一方面,对于边界值的构造,由于课程组设置的各种数值的范围,边界情况的可能性非常少。仅对jml规格而言,
边界条件主要是Tag中的人数的1111的限制。根据数据类型的边界值,仅有idint的边界值,是合法的。
其余数据的0也不需要特殊的处理。

  1. 作业要求编写的测试部分

对于课程要求的junit测试,由于jml中没有signal的要求,主要还是对正确性和其他修改的检验。
我的构造思路是,基于随机数据构造,在第一次和第二次作业中,先随机构造三种类型的图,
分别是稀疏图,稠密图,和树上随机加边的图,以此覆盖结果较少(由于随机生成可能为0),结果较多和结果较为确定的较少情况,
并设计了额外的加人,加边,删除边的数据构造,以此覆盖一些按照变化更新答案的可能策略,并进一步加大测试数据量。

对于第三次作业中的数据构造,由于与图的结构关系不大,选择了直接构造完全图,随机添加、发送消息,
并且针对性的,对主要涉及到的emojiMessage,额外添加,发送了大量的此类消息。

并且在目标函数的测试上,即使使用了50组测试,单次测试中,依然通过上诉策略,在修改后,反复对目标函数进行多次测试。

  1. 互测数据构造

在互测数据的构造上,我认为,由于我们当前的jml规格十分清晰,算法部分较为简单,写错的概率较小。
主要采用了性能测试的数据构造,针对可能的时间复杂度较高的函数,如query_tag_value_sum,query_tag_age_var,
query_couple_sum,query_shortest_path, query_triple_sum, clear_notices,
以及在避免较高查询的复杂度的情况下需要大量维护各种信息的,modify_relation删边的数据进行构造。
以黑盒测试为主,针对性的考虑含有懒操作和不含懒操作的查询构造数据,前者需要每次查询之前,做一点修改,来强制重新计算。
以$O(n^2)$为基础,尽可能最大化操作的复杂度,合理分配添加数据和查询的比例,使得3000条数据能尽可能卡到10秒上界。

此外辅以白盒测试,阅读互测中这些关键函数的实现,构造对应数据。对于某位同学极其奇怪的,我估计为$O(n^2 m)
$的最短路实现,
设计了一个load_network一个完全图,挂一条长度1000的长链,查询约1000次,卡掉了其实现。

  1. 对自己程序的单元测试

除了对其他人的程序进行测试,还需要尽可能保证自己的程序是对的,我主要采用了单元测试的方法,对自己实现的一个容器类,
Tag类的数据进行了构造测试。但还是关注于正确性的方面,没有对network中大量的Exception分支做出测试。
这也导致了第二次作业中,由于AddPersonToTag有一个人的存在没有检测导致了NullException

架构设计与图模型维护

整体架构

整体架构如图。

本单元的整体架构在jml中以及体现得比较清晰了,此处使用了UnionSetSizeSplitTag来维护部分信息。

维护策略

  1. BlockSum 和 isCircle

对于图的两个联通信息,使用并查集确实是非常合理和简单的。但是由于ModifyRelation中,有可能删边,常见的并查集实现,
是无法对一般的删边处理来维护的,于是采用懒操作的方法,删边的时候标记整体为脏,查询的时候,重新建图。重建图的策略选择了
直接将所有边重新加入。不选择dfs或者其他的方式,主要是由于一开始的图很可能高度稠密,大多数点都是联通的,
dfs等方式局部建图和整个重建没有区别。

  1. TripleSum

显然,每次加边或删边的时候,会影响TripleSum,影响的数量恰为边两个点都相邻的点。因此遍历其中一个Person的Acquaintance,
在另一个Person中查询isLinked,如果有,则加一或减一。一个小的优化是,选择size较小的Person遍历。

  1. TagAgeVar

TagAgeVar本身查询为$O(n)$,但是方差的维护也是比较经典的问题了,所以也采取了动态维护的方式。显然,方差相当于
$(\sum x ^ 2 + n \bar{x} ^ 2 - 2 * \bar{x} * \sum x) / n$,显然$\sum x ^ 2$和$\sum x$都可以$O(1)$维护,
因此整体也可以$O(1)$维护。注意这里的除法都是整数的下取整除法,不能直接约去n。

  1. TagValueSum

TagValueSum也应该动态维护,问题是,如何维护。实际上,我们无需遍历所有的Tag,注意到,加边的时候,只会影响到TripleSum
可能影响到的点,即边的两个点的公共相邻点上的Tag。删边时除了加边的影响,还会影响到边上的两个点。
其次,可以采取一种朴素的分块思想,我们只对大于一定size的Tag维护,小于这个size的Tag,查询时直接暴力计算。
这样可以减少最大维护Tag数量,如果将阈值设为15,那么10000条指令最多也只能添加约700个Tag,这样加删边的复杂度不会太高。
(尽管删边的时候仍然可能遍历所有的Tag)。

  1. coupleSum与BestAcquaintance

coupleSum查询互为BestAcquaintance的Person队。BestAcquaintance采用TreeSet维护小根堆,
priorityQueue的删除复杂度不理想。而coupleSum其实只用遍历一边Person,查询其BestAcquaintance
BestAcquaintance是否为自己的数量除2即可。

其余的内容,尤其是第三次作业的维护策略都比较朴素,也不过多阐述。

规格与实现分离

性能问题及其修复情况

在第二次作业的互测中,为了验证某个数据是否正常,我将TagValueSum的计算换成了两重for的暴力计算版本,
第三次作业忘记换回来了,导致性能有问题,没被互测测出来。换回本来的版本即可。

规格与实现分离

我认为,规格与实现分离在以下几个方面有作用:

  1. 从物理上的代码和文档写在不同的文件中,业务代码不会被规格的描述占据过多的篇幅,影响他人阅读自己的代码。
  2. 规格与代码实现的不一致,首先是在性能的方面,许多规格对于正确性的描述,使用了非常长且复杂的循环计算,
    如果代码和规格一致,在性能上很可能无法满足要求。
  3. 其次,在描述上的方面,规格对于某些方法的描述,在实现上很难写出,比如某些逻辑上的\forall\exists
    这种谓词逻辑如果用于某些对象,数组,或者容器,那在实现上,我们可能很难遍历整个对象的所有可能状态。
  4. 而从实现的多样性角度来看,我们实现一个目的,可以有非常多的方式,这也时能够分离的原因。
    使得规格不会影响到我们不断的修改我们的程序,并在原有的程序上增添更多的功能(比如添加边的部分,可以在迭代中维护更多内容)。

通过规格编写Junit测试

在本次作业中通过规格,来编写junit测试。

  1. 首先,应该详细的分析不同的behavior和signal的前置条件,对于每种不同前置条件,都应该构造出对应的测试数据,
    以此,测试出规格中的不同分支是否工作正常。
  2. 对于一致性,首先是ensures中关于返回值,或修改的内容的正确性,可以根据jml内容,编写一个朴素的,正确性验证方法,来校验其正确性。
  3. 其次,不能修改的部分绝对不能被修改,应该写检测方法,校验所有\not_assigned的部分不会被修改,要注意的是,
    比如\old(Message[i]),这个的不修改,不能只保存原本的messages,因为保存的只是引用,需要再手动clone一个,
    校验原本的messagesclone之间的一致性。
  4. 再者,还需要关注不只是函数本身的规格内容,在类之中的invariant不变性规格也是需要检测的。

学习体会

在本单元的学习中,通过学习,使用jml规格,按照jml编写自己的实现与单元测试。首先,对规格的严谨性,清晰性的理解有了认识,
对于规格,在保证方法正确性,便于函数之间的调用与参数的传递,对多人合作编程的作用,在屏蔽函数实现细节,又能向他人展示前置条件,
与严谨的返回值,以及可能产生副作用的结果的强大作用有了认识。

此外,通过编写junit测试,对自己的程序和其他人的程序进行测试,我也对单元测试的概念以及规格化下的单元测试有了更加深刻的理解与认识。

...全文
98 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧