面向对象设计与构造第三单元总结

曾华旭-22371554 学生 2024-05-19 18:15:01

面向对象设计与构造第三单元总结

一、测试过程

  • 1.黑箱测试与白箱测试
    顾名思义,黑箱测试即无需关心内部实现,将程序当成一个封闭黑箱,仅通过输入输出来对程序进行测试的方法。以课程作业的测试为例,生成一些数据投喂给jar包,然后对输出进行合法性检验/对拍,判断正确性和验证性能的方法即为黑箱测试。
    黑箱测试的主要目的就是验证功能和性能。为了保证黑箱测试的效果,生成的数据需要覆盖全面,既需要典型数据,也需要边界数据、异常数据、大量数据等。比如说在关系网络中,既要有大量的人、关系、标签、消息的正常添加和操作,还要有各种异常行为;关系的值的修改后既要有正值,也要有零和负值,tag人数既要有正又要有0等
    黑箱测试的主要优点在于测试和实现是完全分离的,在需求手册确定的情况下测试程序和用户程序就可以分给不同的人完成,效率比较高;缺点则在于测出bug以后进行修复还要大量额外的工作以定位bug。
    白箱测试就是把代码当成一个透明盒子进行测试,测试人员可以看到和访问软件实现代码和内部逻辑;测试人员根据软件的内部逻辑来设计测试用例,以验证软件的内部操作是否符合预期
    白箱测试需要代码实现完以后进行测试,而且除了阅读需求手册外还要理解代码,尝试覆盖大部分甚至全部代码区域,找到代码的漏洞。因此白箱测试实现更加复杂,而优点在于从内部实现的角度进一步确保的软件的可靠性,同时有利于定位bug。比如上个学期先导课中对行覆盖率、分支覆盖率等提出明确要求的测试,如果有效实现了,达到确认每个分支的正确性的效果,那就是白箱测试;本单元中利用JML建模实现的junit单元测试,由于是针对一个函数进行的测试,也可以看作具有一定白箱的属性。
  • 2.单元测试与集成测试
    单元测试即对代码的单个模块的功能进行测试。单个模块可以是类或方法等,这是为了测试每一个模块的正确性,提高组件的可靠性,常常和开发过程一同进行。进行单元测试有利于在代码实现的前期就发现一些问题,避免使整体检查十分繁琐,这尤其对大型的软件尤其重要。比如在本作业的实现过程中,写完一部分函数就可以采用junit进行单元测试;比如在OS课程中,往往需要实现若某个功能就进行充分测试,否则会给将来的增量开发带来莫名其妙的问题。
    和单元测试相对,指的是在单元测试的基础上测试几个模块组合起来的功能是否正确。之所以要有集成测试,主要是因为单个模块的功能可能看不出问题,但是各个模块之间集成起来协同交互的时候、函数互相调用的过程可能存在问题。在作业中,常用的针对整个程序进行测试的办法就是集成测试。
    在我们的作业中,由于规模不大,因此不进行单元测试直接进行集成测试,由于需要写的测试代码少,可能反而比较方便。但是代码规模如果比较庞大,不在单元测试的基础上进行集成测试可能导致找bug工作量极大,最终难以修复的问题。
  • 3.功能测试
    对代码预期实现的功能即实现的正确性进行的测试,不关心代码的性能如时间复杂度和空间复杂度等。比如在作业中测试queryxxx的功能是否符合预期。由于功能测试是数据点驱动的,因此一般是黑盒测试,只需考虑需要测试的各个功能,不需要考虑整个软件的内部结构及代码。
  • 4.压力测试
    评估软件系统在面临高负载或其他压力条件下的性能、稳定性和可靠性。找出潜在的崩溃点或性能瓶颈,为系统的扩展和升级提供数据支持。常用于测试性能,比如在我们的作业中设计搞复杂度、可能导致并查集不平衡的数据等进行压力测试,测试是否会超时。
  • 5.回归测试
    代码在开发过程中往往不断迭代,迭代时实现了新的功能,但可能会影响之前的功能的实现,这就需要检测以往的功能是否还能够正确实现,排除迭代时引入了对之前功能的损害。比如在我们作业的迭代过程中,除了要测试新增的命令的功能是否正确实现,还要测试前几次作业的功能是不是正确实现,有没有被改错;课程组在进行bug修复正确性判定的时候,要测试出现bug的地方有没有被修复,更要测试原来没有错误的测试点是不是依然正确。这都属于回归测试的范畴。
  • 7.测试过程和数据构造策略
    本次作业中可以先用单元测试测试每个方法的正确性,再测试整体的功能。
    在单元测试中,进行数据构造的时候要注意的是数据的覆盖全面性。如上所述,需要有正值、负值、零等。我对value等涉及大小的值依然采用随机生成,不过由于数据量较大,可以保证覆盖各方面的数据。
    在功能集成测试当中,可以把命令分成修改命令和无修改的命令(如query等),然后把一系列修改命令打包变成一组进行状态改变的操作,然后进行查询,测试命令实现是否正确。需要注意的是在我们自己的测试中,可以进行全面的query。比如,在进行一系列添加、修改关系的操作/一系列加入、移出标签的之后对所有个体进行qba操作。这样能够保证全面正确。不过缺点则是数据量过大,测试数据往往达到几十万行,测试时间有一点长,定位错误也有点麻烦(这里可以使用grep等命令行操作进行筛选)。

二、与形式化设计结合的junit测试

  • 1.设计方法
    由于JML已经规格已经给出,用junit实现测试实际上十分容易,直接把JML语言一一翻译成java语言即可,如forall/exit翻译成循环语句,函数调用只需调用对应函数即可。这样设计只要JML的规定符合需求,junit测试代码就是正确的。
    需要注意有许多函数有pure的要求,即函数没有任何的副作用。此处的测试有点麻烦,需要递归比较方法执行前后,方法中全体能够拿到的对象的各种属性。这意味着需要在方法执行前进行深克隆,不过由于使用的类并不支持进行深克隆,因此可以采用“影子网络”,相当于在网络建立的时候就同步建立了一模一样的网络,达到了和深克隆一样的效果。
  • 2.效果分析
    需要注意,junit测试是通过测试的方法对程序的正确性进行验证,而不是对写的代码实现了形式化验证。由于可能输入的测试数据的组合是无穷尽的,这意味着不论怎么测试依然无法保证代码的完全正确性。比如,程序完全可以是不可重入的,在操作的人的id符合某些条件时随机注入一些错误。
    这同时也说明,jnuit测试的效果取决于生成的数据的强度。在我们进行的有限的测试中,可以认为生成的测试数据如果能够覆盖jnuit测试代码的每个分支,就已经达到了测试效果。这是因为存在许多数据和许多次运行,能使代码的实现结果都符合JML规格的每一个要求(对应测试代码的每个分支),那么只要数据还比较多那我们就可以近似认为所有数据,都能使代码符合JML的每个规格要求。如果达到了这个要求,基本上能测出来bug,至少能够通过jnuit测试。

三、架构设计

  • 1.整体设计
    本单元采用了形式化的设计手段,而且已经将核心类的关系、需要实现的函数和函数实现的形式逻辑指出来了,因此在架构设计上不需要费太大功夫。需要注意的是,Network类比较庞大,因此为了不超过500行保证类的单一职责,可以把一部分不修改类的属性的方法交给新的类来实现,比如一些只是进行辅助的pure方法,方法中只涉及对person的方法的调用但是逻辑比较复杂的部分等。下面是第三次作业结束后的整体架构UML图。

img

  • 2.图的维护策略
    在本次作业中,我用图的结构实现了社交关系网络,即根据JML的形式化设计,把每个人作为一个节点,每个人保存了和他相邻的所有人的引用和与之相关的值,保存了所有标签,保存了所有接受到的信息和相关的属性。网络类则维护了一些全局的对象,比如说消息、表情包热度等。此外,还有针对性能维护了一些数据,这将在下个部分展开。

四、性能分析

  • 1.性能分析
    由于设计相对完善,在公测互测中,我幸运地没有被找到性能问题。下面分析每一条单纯命令(帮忙其他命令维护的部分在其他命令处分析)的时间复杂度和可能需要使用某些容器/一定算法/动态维护的命令。

    • ap Network中用<id,person>的HashMap,存入复杂度O(1),无危险。
    • ar acquaintance用<id,person>的HashMap,分别向两个人添加关系,O(1),无危险。
    • mr 修改person中的acquaintance是O(1),但是如果移除了relation,需要将自己从同自己关系移除的人的tag中移除,则达到O(tags)。
    • qv value用HashMap存,复杂度O(1)。
    • qc 用到并查集实现,复杂度是节点到并查集根节点的复杂度。并查集在删边的时候十分麻烦,会给mr加上O(person + relation)的复杂度,不过是线性的,不会超时。加边的时候也需要注意保证均衡性,否则可能会生成一整条链,导致qbs可能超时,因此采用了路径压缩+随机合并的办法进行优化。
    • qbs 使用并查集,只要搜索所有有几个根节点就可以,由于路径压缩的实现,人数足够多时复杂度为O(person)。
    • qts 使用维护的方法,加边/删边的时候看看两个人有没有和同一个人相连,复杂度是O(1)。给mr和ar加上了O(acquaintance)的复杂度。
    • at 用HashMap在Person内存Tag,O(1)。
    • dt O(1)加上维护。
    • att O(1)加上维护。
    • dft O(1)加上维护。
    • qtvs 直接查询很容易超时,在加边、改边、加人进/删人出tag时进行维护,加人/删人比较好维护,复杂度O(1)。加边删边比较复杂,如果暴力搜索所有认识的人的tag,则复杂度是O(tags * perons),达到平方级,可能有点危险,因此我在网络全局维护了一个<Person,HashSet>的HashMap,使得增加的时间复杂度降到O(tags),并且需要维护的地方仅为ap,at,dt,att,dft,dt复杂度增加O(persons),其余复杂度增加O(1)
    • qtav 暴力查询,复杂度不超过1111,实践证明不超时。
    • qba 维护以达到O(1),加/删关系的时候修改即可。我没有使用大顶堆,删除的时候需要遍历找到最大
    • qcs 循环调用qba查找即可,O(person)。
    • qsp 广度优先搜索即可,O(person + relation)。
    • am O(1)。
    • sm O(1),表情包热度用HashMap存即可。
    • qsv O(1)。
    • qrm O(1),注意需要有序获得,因此只能用有序的数据结构存储message,结合message只有增加和删除,没有查找指定索引的操作,因此用了LinkedList。
    • arem O(1)。
    • anm O(1)。
    • cn O(messages)。
    • aem O(1)。
    • sei O(1),这里其实不用再用一个HashSet来存表情包id,只需要初始化表情包热度容器即可。
    • qp O(1)。
    • dce 为了加快速度,我维护了一个emoji2messge的容器保存每个emoji对应的包含它的全体message,因此复杂度是O(emoji + message),需要注意这里不要写成双重循环(emoji * message),否则会超时。维护所涉及的复杂度均为O(1)。
    • qm O(1)。
  • 2.规格与实现分离
    规格与实现分离在本次作业中主要体现为JML规格只是形式化地规定了本方法需要实现的功能,而没有指定具体怎么实现,具体的实现还需要由设计者设计,有可能设计时代码的逻辑和原来的规格逻辑很不相同。事实上,如果直接按照上面的形式直接实现,往往性能十分糟糕,甚至于有时候不能够实现,比如说最短路径的规格只是说明要找到一条最短的符合要求的节点序列,实际上没有办法找到全体符合要求的序列以后再找最短的,因为符合要求的序列有无穷多条。
    但是之所以还要有规格是要给实现一个无二义的指导,以确保在设计阶段无bug。另外正如上面提到的,规格能也保证了测试和实现的分离。

五、学习体会

  • 通过学习体会到了形式化设计的特点。通过形式化设计,设计过程和实现过程很好地分离开来了,并且能够在设计阶段有效地避免一些不必要的bug。但是也存在一些缺点,别如说阅读理解很复杂的形式化语言有一定难度,并且设计者将设计用形式化语言表达出来的过程也存在一定挑战,虽然形式语言是没有二义性的,但是设计者在设计时把设计思路翻译成形式语言的时候可能存在形式语言不符合设计本意的问题。
  • 学到了一些简单的算法设计思路,比如使用合适容器(数据结构),缓存/维护等。
...全文
168 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

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

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