2024 OO Unit3

陆辰-22371569 学生 2024-05-16 13:12:01

2024 OO Unit3


OO Unit3主题为入门级JML规格理解与代码实现,实现简单社交关系的模拟和查询。

本单元给出了详细的JML规格文档,对程序中出现的各个类、属性、方法都做出了形式化描述,实际上确定了本次作业的整体架构。主要的难点在于正确解读JML并实现,且考虑性能的优化。

测试过程

黑箱测试、白箱测试

黑箱测试 是从外部对软件进行的测试。 黑箱测试 将软件视作一个不可窥探的黑箱,软件具体如何处理输入数据对于外界是不可知的,外界可以探知的只有软件最后输出的结果。在进行测试时,测试人员不关心软件内部的结构和实现,只关心输入和输出。测试人员唯一可以依靠的就是软件规格说明书或JML,检查软件能否正确地达成规格说明的要求。

一般而言, 黑箱测试 需要考虑输入的等价类划分,边界情况测试等,力求测试到所有输入的等价情况。

黑箱测试 的优点是构造的测试样例与软件实现无关,即便软件实现更改也可以使用。缺点在于难以完全覆盖输入。

白箱测试 是从内部对软件进行测试。 白箱测试 将软件视作一个完全透明的白箱,完全获知软件内部的结构与逻辑,进而可以对软件的每一个动作进行检测。在进行测试时,测试人员不关心软件的功能与输入输出,只关注软件是否正确地执行了规格所规定的动作。测试人员此时已经有了软件规格说明书与软件的具体实现。

一般而言, 白箱测试 需要穷举程序内部所有的逻辑执行路径,以确保程序内部动作均正确执行。

白箱测试 的优点在于可以定位到源代码的具体错误位置,定位准确。缺点在于复杂耗时且与软件实现高度耦合。

本单元编写JUNIT对函数进行测试的时候,我主要采取的是 黑箱测试 ,因为评测样例代码的实现方式是完全不可知的,必须从规格给定的输入输出入手,构造等价类和边界条件检查是否功能正确,是否出现了不希望出现的副作用等。

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

单元测试 是对软件的最小设计单元/模块进行测试,确保组成软件的小模块内部正确运行。这样可以尽早解决细小的漏洞,后续只需要关注模块间是否正确合作。

功能测试 又称行为测试,检测软件是否按照规定的功能运行,是否达成了要求的所有功能。 功能测试 检查正确性,而不考虑性能开销。

集成测试 又称组合调试,是对组合起来的程序模块进行测试,检查软件之间的接口功能是否正确实现,模块之间是否正确协作。

压力测试 是在计算资源匮乏时进行测试,检测软件能否在计算资源受限的情况下稳定运行,并达到预期的性能指标。 压力测试 的目的是在保证性能在可接受的范围内,软件能够承受的最大负载。

回归测试 是在修改软件代码之后,为了防止修改的部分产生新的错误,重新测试之前已经测试过的内容,以保证这次修改没有引进新的错误。

在本次作业中,本地编写的JUNIT主要进行 单元测试功能测试,检测一个类中的某一个函数是否正确执行。使用指导书的样例和中测进行测试则起到 进程测试 的功能。 强测和互测主要作为 压力测试 出现,使用大规模的数据检测程序的性能瓶颈。而在bug修复中需要保证修复bug时不引入新bug, 体现了 回归测试 的作用。

数据构造策略

本次作业在JUNIT环节由于不可能得知评测代码的结构,所以必须进行 黑箱测试 。我没有选择进行多组随机数据生成,原因有二点。

第一,虽然对于一个类的单个函数而言输入输出和效果是确定的,对于大规模的 集成测试 而言并不是一条一条语句进行测试,之前的输入会影响后面输入的输出结果。如果进行随机输入生成,那么我就需要预知对于每一个输入应该输出的结果是什么。鉴于输入的随机性,必然需要一个绝对正确的样例程序计算正确的输出。但是我们测试的目的是为了保证代码的正确性,而为了进行测试我们又需要一份正确的代码,这样就陷入了死循环。

第二,随机输入的生成也需要进行规划,合理地覆盖边界条件,尽可能全面地覆盖代码运行逻辑。这样输入看似随机生成,实际上还需要人工进行大量的限制,包括确保边界数据的出现,覆盖多种情况(稀疏图、稠密图、增删改查边等),实际上构造生产的难度和工作量都不容小觑。

基于以上两点,我选择了手动构造数据,这样可以事先预知正确结果而不需要有一份正确的代码。手动构造的数据也需要尽可能覆盖所有情况,特别是针对函数自身的特性,保证发现代码的错误。

架构设计

本单元无需自己设计架构,JML已经描述了一个 Network-Person-Tag 的架构。 Network 中储存所有的 Person 和未发送的 Message ,每个 Person 内储存收到的 Message 和一些 Tag ,每个 Tag 储存一些与这个 Person 相关联的 Person

需要维护的图模型只有 Network 中的 Person 。可以将 Network 视作一个 无向图Person 是图中的节点。图中可以增加节点、增加/删除/修改节点之间的关系。我没有特意维护一个图模型,因为可以直接调用 PersonisLinked 方法找出与之相连的其他节点,相当于一个 无向图邻接矩阵 。以下的操作都是处于性能考虑而增加的。

首先在 Person 中创建一个 PriorityQueue 对连接的节点进行排序,同时动态维护,目的是在 queryBestAcquaintance 时复杂度降为O(1)。

其次在 Network 中维护了一个 Person 并查集,在删边时只重新建立原先这条边所在的集合,目的是为了快速实现 isCircle

性能、规格、实现

本次作业出现最大的性能问题在于 queryTagValueSum 。按照规格,这个函数需要进行复杂度O(n^2)的遍历,然而这样在性能上的表现是不可接受的。我原先以为这是不需要担心的,但是强测错了我才知道需要优化。优化的方法在于将查询的开销平均分配到增加/删除/修改节点之间的关系上,这样会让增删查改的复杂度上升到最高O(n),但是查询的开销降低到了O(1)。

我认为优化与否取决于输入数据的类型,例如输入数据中仅有开头有少数的 queryTagValueSum ,绝大多数都是增删改的时候,上述优化就是负优化。虽然优化需要考虑最差情况,但是我还是觉得最重要的是应用场景,当实际有性能瓶颈出现的时候再去做针对性的优化。

从此也可以看出,规格实际上只对功能的正确性有约束,对于实现的方式和性能几乎没有任何约束。也就是说,规格是辅助编程人员理解设计人员的思路的工具,而具体如何实现实际上是完全可以有编程人员自己决定的,只要最后暴露出的接口满足了规格要求即可。

JUNIT与规格检验

在编写JUNIT时可以根据规格来检验。对于 invariant 语句,就在JUNIT中检验执行方法前后是否不变;对于 assignable 语句,就在JUNIT中检查是否正确进行了增删改。可以说,JUNIT完全是可以按照规格进行书写的。

利用JUNIT检验代码与规格的一致性,最大的优点的方便,可以一键执行,而且在代码进行修改后也可以快速进行 回归测试 ,准确度很高。

学习体会

本单元由于给出了JML,实际上已经完成了设计的任务,我所需要做的就是阅读JML并理解,然后进行代码实现。在这个过程中最难捉摸的就是优化需要做到什么程度。很多时候真的感觉只能依靠数据点才能知道。

另外,因为JML繁复又需要严谨,很多时候写了一半又会发现JML更新了,就需要重新再看一遍写一遍,经常会因为忘记更新官方包出错,实现了一遍错误的代码(; ;)。

因为本单元不考察设计,所以难点全在算法上了,我自己是不太懂太复杂的算法的,基本就是随查随用,每次写作业都有点慌的,毕竟多写多错,你永远不知道找到的下一个bug是不是最后一个bug。更恐怖的是多个bug耦合在一起,导致看似毫不相关的几处代码愣是只改一个地方会使原来正确的数据点过不去,搞得bug修复都不好处理。


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

301

社区成员

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

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