OO_Unit3: 规格化设计
测试分析
黑箱测试
黑箱测试只根据软件的功能需求和规格说明来设计测试用例,测试人员不需要了解程序的内部结构或实现细节。测试的目的是验证软件的功能是否符合预期。由于只需要了解需求和规格说明,并基于此编写测试用例即可。
此测试方法更接近用户的视角,能发现功能性缺陷,类似于oo中强测;然而可能无法覆盖所有代码路径,导致一些内部缺陷未被发现,且测试数据依赖需求文档和规格说明的准确性和完整性。
白箱测试
白箱测试通过检查代码、控制流和数据流来设计测试用例。测试的目的是验证代码的正确性和逻辑。测试人员必须熟悉程序的代码和内部结构,测试用例也是根据代码结构、逻辑路径、条件和分支等编写的;主要测试内部逻辑:验证代码实现是否正确,包括逻辑错误、路径覆盖、循环、条件判断等。可以通过静态分析工具或覆盖率分析工具确保所有代码路径都经过测试。
此方法可以发现隐藏在代码中的逻辑错误和缺陷,提供详细的代码覆盖信息,帮助提高代码质量,有助于优化代码,提升性能;但需要测试人员具备编程和代码分析能力,测试用例编写相对复杂且耗时,且难以模拟用户的实际使用场景。
在实际的软件测试中,黑箱测试和白箱测试通常结合使用,互为补充。黑箱测试从功能和需求的角度出发,确保软件功能正确;白箱测试从代码实现的角度出发,确保代码质量和逻辑正确。通过综合使用两种测试方法,可以更全面地发现软件中的缺陷,提高软件的可靠性和稳定性。
单元测试
对软件中的最小可测试部分(比如一个函数或方法)进行测试,以确保其功能正确。
- 粒度最小:测试的对象是代码的最小单元,可以快速得到反馈。
- 白箱测试:通常由开发人员编写,自动化测试代码的内部逻辑,比如我们作业的Junit。
功能测试
通过测试软件的功能和行为,验证其是否满足需求和规格说明;目的是确保软件功能按预期工作,验证功能需求和业务规则的实现。
黑箱测试:不关注内部实现,只关注输入输出和功能表现。
基于需求:测试用例根据需求文档和规格说明编写。
用户视角:模拟用户操作,验证软件的功能。
集成测试
在单元测试的基础上,将多个单元模块集成在一起进行测试,验证模块组合后的功能和性能,以确保它们之间的接口和交互正确。用于测试模块之间的交互和接口,可以包含黑箱和白箱测试的元素。
压力测试
通过施加超出正常工作负荷的压力,测试系统在极端条件下的表现和稳定性。通常是施加高负载、高并发等极端条件,关注系统的响应时间、吞吐量、稳定性等性能指标。类似于我们平时强测和互测中构造出一些极端的数据刀人或者测试自己程序的性能。
回归测试
指在软件发生变更后(如修复缺陷、添加新功能、优化代码),重新测试系统,以确保变更没有引入新的缺陷。对已有功能的再测试,涵盖受变更影响的所有功能模块。目的是确保新代码的变更没有破坏已有功能,验证缺陷修复和新功能实现的正确性。
每种测试类型在软件开发和测试过程中都有其特定的作用和目标,结合使用这些测试方法,可以全面保障软件的质量和可靠性。
数据构造策略
手捏数据:实际上有很多bug是一些很简单的数据就可以测试出来的,并不需要很复杂的数据。就算是用数据生成器得到的几千行输入,我们也希望能将其简化以减轻debug盯着屏幕的痛苦。比如在hw9的qbs
指令,我们就可以通过先简单地构造出少量块分离和合并的基础数据,从而明确自己的程序是在何种情况下会发生问题,而没有必要对着几千行的数据发愁。
对于一些函数如果有tle的忧虑可以尽量优化算法,但鉴于与本单元主题关系不大,好像对今年对时间的要求没有卡的那么死。比如我的valueSum
没有采用动态维护,cpu时间最长似乎是4-5s的样子。
还有就是生成更强的数据时注意一些边界条件的设定,比如Tag的除0,id为负等等,还是得细心阅读jml,好好检查。
规格与实现分离
本次单元三次作业强测互测都没有出现问题。然而,在完成作业的时候总会怀疑各种地方会不会超时,也不知道课程组会卡时间到什么程度,然后就权衡很久该采用什么策略…..
需要注意的是规格和实现分离,课程组给的jml实际上只是用最朴素直接的语言告诉程序的设计者最终需要怎么样的需求,至于具体如何实现,则完全和jml的表述无关,完全由我们自己来决定每个功能如何实现。
比如,为了实现$$O(1)$$的复杂度进行查找,我们大量使用了Map这一数据结构。我在实现并查集时单独引入了类DisjiontSet
,在实现dijkstra
查找最短路径的时候引入了Node
类来记录各个节点,这也充分体现了规格和具体实现分离的思想。
架构设计
hw9
在我的实现下,首先需要明确:并查集本身并不知道两个节点之间是否认识,通过find
找到的父亲相等只代表两者是可达的(isCircle)。所以并查集的作用只是:判断两个人是否可达,而非是否认识,只是为了维护Block的个数。
使用延迟重建整个并查集的思路:在MyNetwork类中设置一个脏位needRebuild
,如果有删边操作就把脏位设置为true
,这时认为整个并查集已经失效。
具体思路如下,是一个简单易行的做法。
指令 | 操作 |
---|---|
ar(加边) | 如果脏位为0,则维护并查集(进行merge);否则不维护 |
mr(删边) | 将脏位设置为1,此时认为整个并查集已经失效 |
询问(isCircle和qbs) | 如果脏位为1,重建整个并查集,否则并查集有效 |
hw10
主要加了一个queryShortestPath
指令,我采用了迪杰斯特拉计算最短路径的方法。缺点就是跑一遍的话出发点到所有路径的最短距离都跑出来了,但实际上只会用到一个。所以我设置了一个脏位。每次跑出来的结果用一个Map来存储:<出发点id,<其他所有点,出发点到该点的距离>>
,对以下两种指令的应对方法如下:
ar
:只加了孤立点,到任何点的距离都为正无穷,反映到之前存储的map就是不存在key,抛异常即可mr(删边情况)/ar
:设置脏位,下次查询需要重新跑dijkstra。
其余一些询问的指令我都采用了动态维护,但由于valueSum
有些麻烦就摆烂了,遍历了下熟人,复杂度$$O(mn)$$
hw11
最愉快的一集,舍友一晚上写完了甚至不敢相信自己。只增加了一些常规的操作,只要按照jml细心地一步一步写问题就不大。
然而后来自己的代码超过500行了,为了满足checkStyle无奈只能新建一个类再继续写。。
Junit测试
需要每次作业为一个函数写Junit测试。
Junit主要涉及数据构造和规格检查。由于我没有尝试在本地对我的Junit代码进行测试,导致我在编写Junit测试的时候心里完全没底,也只能通过交上去的反馈判断正误。
对于构造数据,在第一次作业我尝试使用了多次跑随机数据,效果也还好;不过后来都是只构造了一组较为复杂的数据。至于规格检查,由于不知道课程组提供的错误代码究竟会错到什么程度,我们必须按照jml规格一个字不差地进行最全面的检查。
学习体会
转眼间oo第三单元也接近尾声,相比前两个单元较大的思维量,这一单元更多注重的是代码的严谨和质量。jml类似于逻辑表达式的方式非常严谨,但也导致(对我来说)可读性不那么强。这一单元下来我也掌握了基于jml规格的项目开发流程,学会了最基本jml的阅读和书写,了解并实践了测试的流程,并且体会到了规格与设计分离的思想。
感谢267的3位神仙舍友以及提供评测机的大佬们orzzz