BUAA-OO-UNIT3-2024春

在本单元主要学习了基于JML(Java Modeling Language)的契约式编程,并通过实现一个简单的社交网络了解图论中的算法。

代码测试

对代码做测试是软件开发中重要不可缺少的一个环节,通过本单元的学习加深对各种测试类型的理解。

黑箱测试与白箱测试

  • 黑箱测试:黑箱测试又叫功能测试、数据驱动测试或基于需求规格说明书的功能测试,注重于测试软件的功能性需求。在测试中,把程序看作一个不能打开的黑箱,在完全不考虑程序内部结构和内部特性的情况下,在程序接口进行测试,它只检查程序功能是否按照需求规格说明书的规定正常使用,程序是否能适当地接收输入数据而产生正确的输出信息。黑箱测试着眼于程序外部结构,而通常不考虑内部逻辑结构。
  • 白箱测试:白箱测试又称结构测试、透明盒测试、逻辑驱动测试或基于代码的测试。白箱测试是一种测试用例设计方法,箱子指的是被测试的软件,白箱指的是箱子是可视的,即清楚箱子内部的东西以及里面是如何运作的。白箱测试全面了解程序内部逻辑结构、对所有逻辑路径进行测试。

如何理解各种测试

  • 单元测试:指对软件中的最小可测试单元进行检查和验证,在面向过程的语言如C中,这个“单元”可能是一个函数,而在面向对象的语言如Java中,“单元”可以是一个甚至是一个类中的某个方法。通过单元测试,可以将一个巨大的软件系统拆解,分别测试其各个组成部分,通常工作量较大。
  • 功能测试:根据产品特性、操作描述和用户方案,测试一个产品的特性和可操作行为以确定它们满足设计需求。功能测试是为了确保程序以期望的方式运行而按功能要求对软件进行的测试,通过对一个系统的所有的特性和功能都进行测试确保符合需求和规范。功能测试只需考虑需要测试的各个功能,不需要考虑整个软件的内部结构及代码。一般从软件产品的界面、架构出发,按照需求编写出来的测试用例,输入数据在预期结果和实际结果之间进行评测。
  • 集成测试:集成测试是单元测试的逻辑扩展。实践表明,一些模块虽然能够单独地工作,但并不能保证连接起来也能正常的工作。一些局部反映不出来的问题,在全局上很可能暴露出来。因此需要在单元测试的基础上,将所有模块按照设计要求(如结构图)组装成为子系统或系统,进行集成测试。
  • 压力测试:压力测试也称为强度测试、负载测试,是模拟实际应用的软硬件环境及用户使用过程的系统负荷,长时间或超大负荷地运行测试软件,来测试被测系统的性能、可靠性、稳定性等。例如课程作业中的强测便是一种压力测试,通过大量包含高复杂度、极端情况的数据点来测试程序的鲁棒性,以发现程序可能存在的瓶颈和缺陷。
  • 回归测试:回归测试是指修改了旧代码后,重新进行测试以确认修改没有引入新的错误或导致其他代码产生错误。例如课程作业的bug修复环节就是典型的回归测试,修改某个bug后必须保证原来通过的测试点仍能通过,否则很可能引入了新的bug。

数据构造策略

首先,通过大量随机生成数据,尽量覆盖所有可能情况,重点在于覆盖的全面性;但随机测试不可避免带有偶然性,所以还需要构造边界数据,确保程序在极端情况下的鲁棒性;其次,针对高复杂度的指令,还要构造压力测试数据,尤其对于新增的指令,通过压力测试考虑有无优化的必要;最后,还要构造非法数据,测试程序处理异常的能力。

架构分析

本单元的架构几乎不用自己设计,只需按照课程组给出的JML实现即可。但是由于本单元需要实现基于图论的社交网络,所以合适地选择数据结构与算法对程序的性能起着至关重要的作用。首先给出本单元的最终类图,然后再深入讨论图模型的构建和维护策略。

图的构建与维护

基于社交网络中Person类、Tag类和Message类的id的唯一性,本单元几乎所有的JML给出的“数组”类型的容器都使用HashMap这一数据结构来实现,使得在需要频繁查找时也能获得较高的时间效率。只有在实现Person类中的messages数组时使用了链表LinkedList这一数据结构,这是因为Person类中的getReceivedMessages()方法需要一个有序的容器来存储messages,并且没有查询操作,只有插入和删除,所以相较于同样有序的ArrayList,链表的时间效率更高。

面对hw9中的query_circle和query_block_sum指令,需要判断两个结点是否处于同一个联通子图中,以及求出整个社交网络图中的极大联通子图的数目。这里采用了并查集的方法,本质上是动态维护的思想,在每次添加节点、添加边、删除边时,都要进行维护,尤其是删边时需要重构并查集。在查询指令占比较大,而删边情况出现较少时,并查集的方法将体现出良好的时间性能。

对于query_triple_sum和query_tag_value_sum指令同样需要采用动态维护的方法,以及query_couple_sum指令,值得注意的是有些指令动态维护的逻辑十分复杂,必须保证考虑全面,否则很容易出现错误。

对于query_shortest_path指令,我选择了双向BFS算法来实现求两个结点之间的最短路径的长度,当然首先会想到使用Dijkstra算法求最短路径,但鉴于Dijkstra算法具有O(n^2)的时间复杂度,且其本质是广度优先搜索的思想,所以具体实现使用双向BFS将搜索时间减半。

规格与实现的分离

规格的一个很重要的要求是严谨性,书写规格的人员不会考虑具体如何实现,只需要使用简单的数据结构和简单的算法严谨地向程序员说明需要实现的功能即可。但是,实现则不仅要考虑程序的正确性,还要思考如何在保证正确性的前提下提高程序的性能,使程序高效地运行。而且往往在那些容错限度较大的场景中,自然语言的描述已经能满足需求,这提示我们规格和实现能够分离,也必须要分离。

从我自己作业中出现的性能问题来看,在hw10中的query_tag_value_sum指令出现了超时的问题,仔细分析原因可知是实现时采用了JML中描述的二重循环,以及对isLinked()函数调用的时间复杂度过高。当我在MyTag类中建立并维护valueSum这一变量后,超时的问题便可得以解决。由此可见,正确掌握规格与实现相分离的思想对于提高程序性能至关重要。

Junit测试与规格

在本单元的作业中,可以是规格就是Junit测试的唯一指导,在保证规格正确的前提下,严格对照规格实现的Junit测试也必然是正确的,我想这也就是形式化设计的意义和魅力所在。具体而言,就要保证Junit测试检验代码实现与规格的一致性,我在实现对hw11中的deleteColdEmoji的测试时对一致性有了很深的体会。在deleteColdEmoji的JML规格中,有8个后置条件ensures,只需在Junit测试代码中对这8个后置条件逐一验证即可,事实证明这样做能够保证决定的正确性。当然如果某个方法还有pure的只读标签,还需验证所涉及到的对象是否发生改变。

总的来说,充分利用规格信息,考虑全面测试用例,便能做到有作用有意义的Junit测试。

学习体会

通过第三单元的学习,对契约式编程有了初步了解,并结合阅读JML规格的实践,锻炼了将形式化表述的规格转化为具体代码功能的能力。同时,对软件开发中的测试这个环节有了更深刻的理解,测试不只是平时课程作业的与评测机打交道,而是一套体系化的理论并具有强大的工程背景。以及当需要对性能提出要求时,规格与实现相分离的重要思想。当然实现过程中还收获了几个图论的算法。

  • 30
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值