BUAA OO 第三单元总结
一、测试过程分析
1.1 黑箱测试 与 白箱测试
- 黑箱测试
- 黑箱测试是一种功能测试方法,测试人员不需要了解软件的内部结构或工作原理。
- 测试人员只关注输入和输出,通过给定的输入来验证软件的输出是否符合预期的规格和功能。
- 黑箱测试关注于软件的功能是否按照需求规格说明书或者用户需求进行操作和输出。
- 测试人员通常会根据软件的规格说明书、需求文档或者用户文档来设计测试用例,从而验证软件的功能是否符合需求。
- 白箱测试
- 白箱测试是一种结构测试方法,测试人员需要了解软件的内部结构和实现细节。
- 测试人员通过检查软件的代码、逻辑路径、控制流程等内部结构来设计测试用例,以验证软件的内部逻辑是否正确。
- 白箱测试通常关注于代码覆盖率、路径覆盖率、条件覆盖率等指标,以确保测试覆盖了软件的各个逻辑路径和条件。
- 白箱测试也被称为结构测试或逻辑驱动测试,因为测试用例的设计是基于软件的内部逻辑结构。
1.2 单元测试、功能测试、集成测试、压力测试、回归测试
- 单元测试
- 单元测试是针对软件中最小的可测试单元进行的测试,通常是函数、方法或类。
- 单元测试旨在验证每个单元的功能是否按预期工作。
- 开发人员通常编写单元测试来确保其代码的各个部分都能正确地执行其预期功能,同时也有助于提高代码的可维护性和可重用性。
- 功能测试
- 功能测试是对软件的功能进行测试,以确保软件的功能符合用户需求和规格说明书。
- 功能测试通常是黑箱测试的一部分,测试人员根据功能需求设计测试用例,验证软件的功能是否按照预期工作。
- 集成测试
- 集成测试是将软件的各个模块或组件结合在一起,测试它们之间的交互和集成是否正常。
- 集成测试旨在验证各个模块之间的接口是否正确、数据传递是否准确,并确保整个系统的功能完整性
- 压力测试
- 压力测试是对软件在极端条件下的性能进行测试,例如高并发、大数据量或长时间运行等。
- 压力测试旨在评估软件在压力下的稳定性和可靠性,以及其性能指标如响应时间、吞吐量等是否满足要求。
- 回归测试
- 回归测试是在软件进行修改、更新或添加新功能后进行的测试,以确保修改后的软件仍然能够正常运行,并且未引入新的错误或破坏现有功能。
- 回归测试通常是自动化的,并且涉及重新运行之前的测试用例,以验证软件的行为是否与之前版本一致。
1.3 数据构造策略
- 使用数据生成器进行随机生成:
- 首先对
network
进行初始化,包括- 第一次作业:
- 随机生成一定数量的
person
,并在部分person
间建立关系 - 使用
load_network
指令(可通过改变value
的生成策略控制图的稠密程度)
- 随机生成一定数量的
- 第二次作业:
- 在第一次作业的基础上,为部分
person
添加tag
,并向tag
中添加一定数量的person
- 使用
addTag
与addToTag
指令实现
- 在第一次作业的基础上,为部分
- 第三次作业
- 在前两次作业的基础上,向
network
中存入一些emojiId
,然后向network
中添加一定数量的message
,并发送其中的一部分 - 使用
storEmojiId
、addMessage
与sendMessage
指令,其中addMessage
包括 红包、表情、通知与普通消息 四种类型与person2person
、person2tag
两种类型
- 在前两次作业的基础上,向
- 第一次作业:
- 之后便是随机生成一定数量的指令
- 大部分的
id
均是在一定范围内随机生成的,可覆盖id不存在、id相同等异常情况
- 大部分的
- 首先对
- 效果分析:
- 由于数据生成的随机性,加上自动化测试的便利性,在大批量数据的测试下,可以很快发现代码中不符合
jml
规范的bug
- 由于数据生成的随机性,加上自动化测试的便利性,在大批量数据的测试下,可以很快发现代码中不符合
- 可改进之处:
- 在随机生成指令时,未考虑部分指令间的关联关系,而是采用纯随机的方式,可能导致无效数据过多
- 考虑通过指令组的形式来更好的生成数据
- 生成指令时可以重点关注本次作业的指令,以便快速发
- 现迭代所产生的bug
- 上调新增指令比例
- 由于未考虑部分极端情况,如大量的
query
操作,导致无法测出程序的性能问题- 考虑更深一步的手动构造,来测试可能出现的超时问题,进而发现设计逻辑上的性能问题
- 在随机生成指令时,未考虑部分指令间的关联关系,而是采用纯随机的方式,可能导致无效数据过多
二、架构设计
-
第一次作业
-
示意图
-
本次作业仅涉及
network
中persons
的维护 -
在
network
中新增属性blocks
与tripleCount
用于维护连通块与三元环数目(此处添加person2Block(Map)
实现id->block的快速访问) -
采用连通块
block
将相互连通的person
放入同一个block
中,以便快速得到person
间的连通关系与block
的数量 -
维护:
addPerson
:新建block
addRelation
:block
合并(person1
、person2
不属于同一block
)、维护tripleCount
modifyRelation
:block
分离(remove
后person1
、person2
不在同一block
)、维护tripleCount
- 由于无法直接判断删边后是否在同一块,需考虑此处判断条件
- 思路1:无论是否在同一块,均根据
remove
前该block
内persons
,新建block
,若一次新建后有剩余person
,则再进行一次新建操作- 不足:稠密图删边后在同一块中的可能性较大,重复建块可能会增加时间开销
- 思路2:先判断二者是否在同一块(通过
DST
、BST
或其他算法),然后决定是否新建block
- 不足:稀疏图删边后在同一块的可能性较小,重复判断可能会导致时间的浪费
- 折衷:可根据
network
的稀疏程度(取决于人数和边数)决定是否要进行判断
-
-
第二次作业
-
示意图
-
本次作业新增对于
person
中tags
的维护 -
在
person
中新增属性bestId
与maxValue
用于对bestAcquaintance
进行维护,新增belongToTags(Set)
用于实现person
->所属tag
的快速访问,Tag
中新增属性ageSum
、age2Sum
、valueSum
、valueSumModified
实现对ageSum
、ageVar
的快速计算以及对于valueSum
的维护(age2Sum
可用于快速计算方差) -
addToTag
:维护belongToTags
、ageSum
、setValueSumModified
-
delFromTag
:维护belongToTags
、ageSum
、setValueSumModified
-
addRelation
:维护所属共同tags
的valueSumModified
,维护bestId
与maxValue
-
modifyRelation
:减边时 维护所属共同tags
的valueSumModified
,维护bestId
与maxValue
- 虽然此处进行维护,但是重新计算时仍需遍历所有
acquaintance
,可通过堆、优先队列 或SortedMap
对acquaintance
与value
进行维护
- 虽然此处进行维护,但是重新计算时仍需遍历所有
-
最短路径可使用
dijkstra
算法解决
-
-
第三次作业
-
示意图
-
本次作业新增对于
messages
的维护 -
本次作业新增对于
person
与network
中messages
的维护以及network
中emojiList
(id->popularity)的维护 -
Message类相关示意图
-
使用数组存储
person
内部messages
(保证收到message
顺序不变)
使用Map存储network
内messages
与emojiList
(便于快速访问) -
注意避免
messageId
与emojiId
的混淆,以及不同种类message
的不同处理方式和类型转换
-
-
异常类实现:
-
构造内部
static
计数器,用于对该类型异常的计数 -
使用
Map
保存不同id
触发该异常的次数 -
id
需根据类名辅助理解 -
示例代码如下:
-
public class MyPersonIdNotFoundException extends PersonIdNotFoundException { private int id; private static int count = 0; private static HashMap<Integer, Integer> id2exceptionTimes = new HashMap<>(); public MyPersonIdNotFoundException(int id) { this.id = id; count++; int time = id2exceptionTimes.getOrDefault(id, 0) + 1; id2exceptionTimes.put(id, time); } @Override public void print() { System.out.println("pinf-" + count + ", " + this.id + "-" + id2exceptionTimes.get(id)); } }
-
-
维护思路:
- 对所有query方法对应的数据均进行维护!
- 直接维护:如
ageSum
,在加减person
时直接得出结果 - 懒惰优化:对相应数据设置脏位,当执行query操作时,再进行计算(适用于计算时间复杂度在O(n2)及以上且不便于直接维护的数据如
tagValueSum
)
三、bug与性能问题分析
- 由于实现严格按照JML约束进行,bug较少且均可通过生成数据快速测出(多为变量名或方法名错误)
- 性能问题
- 第一次作业:
- 最初
blocks
使用**ArrayList
**存储,无法实现person
->block
的快速访问,添加person2Block
(Map)实现id
->block
的快速访问 - 当删边时,未判断删边后是否在同一块中而直接新建
block
,导致modifyRelation
用时过长,在压力测试下会导致超时
- 最初
- 第二次作业:
- 对于
bestId
的维护,需在加减边时进行,可通过脏位记录变化,直到访问时根据脏位判断是否需要更新 - 对于
tagValueSum
的维护,att
与dft
时记录person
所属tag
,若加减边时,对二者共同tag
内的valueSum
进行维护,同时valueSum
的计算可由n2降至n2/2。
- 对于
- 第三次作业:
- 需考虑发红包时
tagSize
为0的情况
- 需考虑发红包时
- 第一次作业:
- 对于规格与实现分离的理解:
- 规格只是对于类中属性与方法的功能性定义,在具体实现中若完全按照规格进行实现,则可能会出现较大的性能问题,我们需要在规格约束的基础上,寻求更好的方式与算法对接口中的方法进行实现,如 利用Map代替数组实现对于数组中元素的快速访问、利用优化后的算法代替规格中的二重或三重循环 以及 通过对结果进行缓存以避免重复计算等问题。
四、Junit测试
-
注意:Junit测试时assert应使用Assert中的方法(如
assertTrue
…)而不能直接用assert进行判断! -
本单元Junit测试主要利用
JML
规格信息来设计(qts、qcs、dce),包括以下几个方面- 方法规格:
- 前置条件:
requires
分类讨论(作业不含)- 正常行为
- 使用
try catch
判断是否出现异常
- 使用
- 异常行为
- 通过
assertThrows
方法判断抛出的异常类型
- 通过
- 正常行为
- 后置条件:
ensures
根据规格约束确定方法调用后满足所有后置条件约束 - 副作用:
assignable
确保只改变副作用范围内的对象状态 pure
方法:确保对象的状态未发生变化(使用strictEquals
方法)
- 前置条件:
- 类型规格:
- 不变式:
invariant
确保不变式恒成立
- 不变式:
- 除此之外,
\not_assigned
(对象)状态保证不发生改变
- 方法规格:
-
对
\old
对象进行克隆以防访问同一个数组的引用而导致判断出错 -
使用
Parameters
:prepareData()
实现自动化 批处理-
参考实验代码,自定义generate相关逻辑以初始化
network
(可参考 数据构造策略 部分) -
自定义测试组数进行测试,随机数据保证较高覆盖率(注意考虑稀疏图与稠密图)
-
示例代码如下:
-
public MyNetworkTest(Network network) { MyNetworkTest.network = network; } @Parameterized.Parameters public static Collection prepareData() { int testNum = 100; //测试次数 Object[][] object = new Object[testNum][]; for (int i = 0; i < testNum; i++) { Network network = generateNetwork(); object[i] = new Object[]{network}; } return Arrays.asList(object); }
-
五、学习体会
-
通过本单元对于JML的学习,对于规格和契约式编程有了更深的理解;通过对于JML代码的阅读,对方法所要实现的功能有了初步了解,然后对于复杂的方法设计算法进行具体实现。
-
然而本单元的前两次强测结果却不尽如人意,按照规格实现会存在较大的超时问题,虽然进行了一定程度的优化,但是仍然存在可优化之处,导致面对大量数据时运行时间过长,究其原因在于
1:在实现时,未充分考虑算法的优化,导致在进行大量重复操作时,运行超时
2:在测试时,只考虑了随机数据,而未对特殊情况进行考虑,未构造出较强数据以至于无法发现自己的性能问题
这也提醒我对于实现算法以及维护方式的设计等思考。