301
社区成员
发帖
与我相关
我的任务
分享黑箱,即不关注具体实现,只通过对外的输入输出接口,给出合法的输入,检查输出是否符合预期。
黑箱测试的过程中,由于没有足够的针对性,只能通过大量随机生成数据,以及一些通用的构造策略(如:正常数据 + 边界数据 + 异常数据)来保障覆盖率,可能有一些特殊数据难以覆盖到。
白箱,即可以看见实现细节,通过检查软件内部的逻辑结构,对软件中的逻辑路径进行覆盖测试。
白箱测试的过程中,需要在程序的不同地方设立检查点,检查程序的状态,以确定实际运行状态与预期状态是否一致。此外,测试过程还要保证对所有分支的覆盖,还包括对代码的静态分析。
但是白箱测试在我们目前的实际应用中有一个问题,即这种测试方法似乎更适合实现者和测试者分离的模式,因为实现者很难意识到自己代码中一些特殊的逻辑漏洞,就难以构造出针对性够强的数据。因此自己进行白箱测试时,要尽量抛开实现时的思路,重新思考设计要求,从而找到更多的潜在问题。
针对每个单元(方法或者类等)分别进行测试,其目的是确保每个单元都能按照预期工作,并且与其他单元之间的接口也是正确的。
单元测试的一个很重要的点就是划定单元,尽量保证单元具有独立性,这也就需要我们在设计的过程中尽可能做到高内聚低耦合。如果单元中存在大量的对其他单元的依赖,或者是划定的单元过大,都会导致难以精确定位 bug。
我们的 Junit 测试就是一种方法层级的单元测试方法。
功能测试,也就是整体的黑箱测试,主要关注软件的功能是否按照需求规格说明书的规定正常使用,其目标是确保软件的功能完整、正确,并且满足用户需求。
我们的公测、强测以及同学们搭建的评测机一般都是在进行功能测试。、
集成测试是在单元测试的基础上,将各个模块按照设计要求组装起来进行测试,以检查模块之间是否存在问题,其目标是检查接口之间的数据传递是否正确,以及模块之间的功能是否协调一致。
集成测试可以分为增量集成测试和整体集成测试,前者是逐步将新模块加入已测试的模块中进行测试,后者是直接将所有模块一次性组装起来进行测试。
压力测试是为了评估系统在异常或峰值负载条件下的性能表现。通过模拟大量的用户请求或数据输入,来测试系统的稳定性、可靠性、可扩展性和响应时间等指标。
压力测试主要针对性能,有助于发现系统的性能瓶颈和潜在问题,为系统优化和升级提供依据。
我们本单元的压力测试主要体现在大量指令、复杂的图结构、频繁进行的修改或查询等,通过手动构造或者大量随机生成(这往往也需要一定的指向性,不能纯随机,否则强度往往不够)数据来进行测试。
回归测试是指当软件或应用程序的某个部分发生变化时,重新运行之前已经通过测试的部分,以确保新代码没有引入新的错误或导致旧代码出现错误,通常用于验证对软件的修改或升级是否没有破坏原有的功能。
本单元中应用回归测试可能更多是在优化算法前后用相同的数据点进行测试,避免性能的优化导致正确性的损失;前两个单元我经常在迭代完成后用上一次的强测数据进行测试,从而避免丢失已有的功能;而本单元由于设计比较严格地遵守了开闭原则,针对原有规格的修改仅有一次(第二次作业增加 tag 后修改 modify_relation 相关要求),还是增量修改,因此基本不必担心实现新要求的过程中损失原有的正确功能。
我的数据构造流程:
【此外在测试 deleteColdEmoji() 时,我还对 limit 进行了随机,包括 0(不删)、所有 heat 的加和(一个很大的数,保证全删)以及所有 heat 的均值(部分删除)】
以上流程中,主要在第三步生成关系集合时有点策略,其它基本就是大量的随机。生成关系时,一方面是避免重复生成浪费时间,每两个人要保证只随机一次;另一方面要保证各种情况的全覆盖——零图、完全图、稠密图以及稀疏图,对此,我采用的方法是控制生成关系的概率——第一组数据概率为 0,最后一组概率为 1,其它设置为 25% 附近和 75% 附近,以覆盖到各种情况。


其中,除了规格中要求的类,为了方便实现功能或者进行优化,我又定义了几个辅助类:
基本就是按照规格要求进行的,通过 MyNetwork.persons 和 MyPerson.acquaintance 两个容器进行维护,相当于用邻接表存储图结构。
由于为了优化连通性查询而构建了并查集,还需要多维护一下 MyNetwork.relations,其实也都比较容易,每次加功能的时候想想是否涉及增删查改,如果涉及就分别处理一下即可。
我在本单元的作业中基本都做到了较为充分的优化,通过学习学长学姐的博客,以及与同学们交流探讨,找到了一些比较合适的优化算法,因此强测和互测都没有出现问题,属于是比较圆满的一个单元了。
优化过程中,主要考虑的就是寻找合适的算法(如双向 BFS)、数据结构(如并查集)以及高效容器(hashmap、hashset 等),另外,针对不容易计算,且需要查询的数值,可以设置一个变量来动态维护,或者设置脏位,只有数据变动之后才重新计算。
但是需要注意的是,维护中间变量的方法极大地简化了查询方法的复杂度,但是却会让相关修改方法的复杂度随之提升,这两者之间需要做好平衡,如果能够分析修改和查找的频率或许可以做出更好的决策,否则就只能按照最坏的可能性考虑了。
这里记录一个我印象比较深刻的优化:关于第二次作业里为了优化 tag_value_sum 的算法多存储了一个 person 加入的 tag 的容器,用来维护 value_sum 这个变量,但是由于全局的 tagId 和 tag 并非一一对应,我又重写了 MyTag 的 HashCode(),把 personId 和 tagId 拼在一起才获得每个 tag 专属的一个编号。这个做法其实并不优雅,很麻烦而且不比每次改关系时遍历公共好友的公共 tag 强啥,更重要的是,这种优化其实已经有些背离了设计者的想法,因此之后如果还有迭代可能还要接着打补丁,因此我觉得魏新明同学讨论时说的“规格里没写的不要随便存”(大概这个意思吧)确实很有道理,毕竟实现是要完全依照设计进行的。
规格与实现相分离,实际上就是设计与实现相分离,即设计者仅负责约束每个模块的功能和输入输出接口,让这个系统组合起来能够完成需求,但是具体用什么方法实现,并不是设计者需要考虑的问题。
设计者给出规格有很多办法,包括用自然语言表述,但是因为自然语言不可避免存在二义性,会被误解,有时候就需要一套标准化的语言,比如我们的 JML。
至于实现者,同样有多种选择,无论是容器、算法还是数据结构,都可以自选,只要最后把代码封装成符合设计规格的黑箱,就算完成任务。
这种理念很适合大项目的合作开发,设计者不用考虑实现方法,只需要将功能分配到哥哥模块,各模块可以交给不同的人实现,这样只要大家都严格依照规格实现功能,就能很方便地合成一个较大的系统,此外也会给测试人员提供便利,让测试程序可以和实现程序同时进行,用并发执行的办法提高开发效率。
我认为规格化设计确实很适合做单元测试,因为约定了规格之后,可以将设计、实现、测试全部分离,实现与测试都仅分别参考设计规格,考虑使用不同的方法计算同一个数据,实现测试方法和实现方法的正交化,避免思维定势带来的逻辑漏洞重复出现,这大大加强了测试的强度。
具体到我们本单元的 Junit 测试,因为测试的时间复杂度要求不高,所以可以放心地直接应用规格中描述的多重循环等实现代码中不可能直接拿来就用的计算方法,这样的方法简单直接,而且能保证正确性,很适合作为一份标准对照,来验证实现是否存在漏洞。
此外,除了返回值的正确性判断之外,还需要严格遵照规格对方法的副作用进行判别。在这方面我倾向于直接按照 JML 一条一条来,如果要求 pure 就针对所有的模型依次检查,否则就对着所有的 ensures 一个一个翻译(JML -> code),这种做法略显笨拙但是不会出错,相比之下帮朋友 debug 的时候发现有人按照自己的理解进行副作用判断(JML -> 自然语言 -> code),中间多了一次转换,这样测试代码看起来简短了一些,但是问题是由于和规格并不一一对应,一旦出现了理解偏差,或者是漏写了一条要求,就很难发现问题,所以我还是更倾向于不会出错的“笨方法”,毕竟单元测试讲究的就是个正确性。
本单元整体感觉比前两单元简单不少,只要沉下心按照 JML 一点点写,至少正确性上不会有问题;而且强测没有性能分,对了就是对了,感觉很舒服。
这单元的主要压力来自两个不确定性——一个是不知道要把性能优化到什么程度才能通过强测,另一个是 Junit 测试点很难本地测试,基本都是脑测之后交上去听天由命。
性能方面,为了保证不 TLE 做了很多优化,有些已经让我的整体架构显得不太优雅了,而且在优化的过程中一直提心吊胆,担心损失正确性。记得在五一出去玩的火车上快睡着了突然想起来不同人之间的 tagid 可能重复,导致我的实现会有隐患,立刻起来修 bug,真是难得的体验,印象深刻。
此外主要说说 Junit 测试点的问题。我在第一次作业的测试中就加入了“影子网络”进行备份(其实就是构造两个数据完全相同但是其中存储的对象全都不同的 network,叫影子纯属上个单元影子电梯叫顺口了),影子网络不调用测试方法,只作为检查 pure 的标准参考,我觉得这种方法理论上应该是万无一失的,但是实际上有一个点会出现“冤假错案”,把正确的判成错的;而放弃影子备份策略,改成只对比同一个 network 调用目标方法前后获取到的属性数组,就能通过测试(这种方法其实没啥意义,因为是浅拷贝,依次对比每个引用对象结果必然相等),很神奇,但是因为最后也没能看见测试点的真容,这个神奇的问题就一直悬而未决了。
另外就是课程组能不能稍微修改以下公测提交的编译检测,把 Junit 不能通过编译的提交也直接打回来,否则因为没看明白课程组要求的文件结构(在 src 里面分包等)浪费掉好几次机会真的挺难受的。
这里稍微谈一下我对于 JML 这样的语言的看法。我认为这种语言应该作为设计规格的“原本”,作为避免误会、确定设计者本意的一套说明,完全可以与简单的自然语言描述结合在一起使用,也就是让实现者除了能看到 JML 之外,也能知道设计者设计每个模块的目的,从而更高效地选择具体实现实现的手段,只有在自然语言表述不清的时候采取诉诸 JML,从而提高开发效率。
总体来说,这单元是 JML 单元,但是 JML 本身似乎是最不重要的一部分知识了,也未必有什么实际价值。真正的语法部分其实不用怎么学,单元开始之前读了公众号推送和学长学姐总结的一些常用语法知识基本就足以应对作业中的全部需求了。但是这单元更多的是让我们体会较大的工程中,设计与实现(还有测试)相分离的理念,我觉得这是很有必要的,现在也确实找到了一些感觉,但是还不够,需要以后一点点继续培养。此外就是可能是课程组觉得这单元因为不用做设计,真正需要动脑的地方太少了,就加了一些性能方面的约束,也让我们复习,以及探索了不少算法知识,虽然有些紧张,但是探索算法、跟朋友们论证可行性、估算实际效果等等过程也挺有意思的,要是没有点压力都不像是 BUAA_OO 了(笑)。