OO2023-U3-JML

测试过程分析

对黑白箱测试的理解

黑箱测试:从软件外部对软件实施的测试,也称功能测试或基于规格说明的测试,不需要了解软件的内部结构和源代码,只关注软件的输入数据和输出结果是否符合要求。

白箱测试:对软件的内部结构和逻辑进行分析后的测试,也称结构测试或逻辑驱动测试,要求测试人员具有编程技能和对软件的深入了解,并能访问所有源代码和文档。

对其他测试类型的理解

单元测试:对软件的最小设计单元/模块进行正确性检验的测试工作,目的是消除局部模块的逻辑和功能上的错误和缺陷,通常采用白盒测试方法,例如这单元编写的 OK test 测试程序。

功能测试:对软件的功能需求进行验证的测试,目的是保证软件满足用户的期望和需求,通常用黑箱测试,例如这单元的中强测。

集成测试:将通过单元测试的模块组装成系统或子系统,并进行接口和功能的测试工作,目的是发现模块间的接口问题和集成后的功能问题,通常采用黑箱和白箱相结合的方法。

压力测试:对软件在特定负载条件下进行性能表现的测试工作,目的是检查软件能否承受高负荷、高并发、高数据量等极限情况。例如第十一次作业强测的第 4 和第 9 个点,以及互测房的狼王课下 hack。

回归测试:在修改了软件代码后重新执行已有测试用例的测试工作,目的是确保修改没有引入新的错误或影响原有功能。

测试工具

这单元的课下测试其实是前面作业最容易实现的,只要考虑数据构造,剩下的找一两位同学对拍。我使用了 python 工具,对拍部分沿用前两个单元,不同的是增加了 jar 包的 CPU 时间记录功能,方便校验课下的复杂度。不仅如此,这单元的测试指令特别多,bug 出现率更高,肉眼发现 bug 概率更低,加上中测实在太弱(看提交的通过率就知道了),因此课下对拍更不能少。

数据构造策略

需要检验:

  1. 指令正常执行时程序内部变量是否正确修改;
  2. 指令触发各种异常时能否抛出对应异常,以及异常输出是否正确;
  3. 能否通过压力测试,人数极多或关系极多或人数和关系数数量级相同等情况下是否超时。

对于前两条的正确性测试,我在 python 中用全局变量 prob_exc 控制数据出现异常的频率。先将该变量调为非 0(也不可过大),超过 1w 条数据测试异常抛出情况。测试完成后,将 prob_exc 调为 0,从而实现更高效率的测试。

对于第三条的压力测试,比较复杂以及容易出错的有 qbs , qts , qba, qcs , qgvs , qlm 等。采用两种大数据测试:大量修改关系网,最后用少量 query 指令,用于针对动态维护;每次修改关系网都进行相应 query ,用于针对每次查询时才进行计算的程序。可怜鼠鼠费尽心机,最后互测时一个 CTLE 都没找到,反而鼠鼠的程序速度是房里倒数的。

提高 bug 出现率的数据生成程序修改:

  1. 两个人之间的关系权值随机性越小,出现 bug 的指令越靠前,部分同学的程序也跑得越慢。看了同学的代码,原因如下:出错概率上升的原因是从 hashMap 取出的 value 是 Integer 型数据,比对时使用了 == 而非 equals 方法,而随机性降低意味着出现相等概率更大,这样 value 值比对就会出错。跑得更慢的原因是程序用了堆优化,猜测值相同时堆的实时维护更耗时,不过都用了堆优化了,我也甭想让它超时。也别太非随机。
  2. 减少 group 个数,限制为 1~3 个。课下测试出 message 和 group 型的 bug 效率更高。
  3. message 专门测试:(构造一些人在 group 里,一些人不在)先 store 足够的 emojiId ,再 add 足够的各类型 message ,再 send 一半的 message ,然后执行少量的删除信息相关的指令(cn, dce)后,紧跟相关的查询指令。完全随机数据要跑出相关 bug 可能跑 10w 条也找不到。对其他的指令测试也可以用类似的思路。

单元架构设计

这单元有个重要的选择:是否动态维护。

第九次作业

复杂度高的指令只有 query_block_sumquery_triple_sum ,对于 qbs 使用并查集动态维护,增加人或关系时用路径压缩的并查集复杂度略大于查询时,查询时复杂度为 O(1)。为什么用动态维护而不是查询时计算?因为就平均而言,并查集动态维护复杂度比查询时计算低,绝对不会超时,并且往届学长也这么用,所以算法小白就这样跟风啦。

query_triple_sum 的复杂度按 JML 实现的话是 O(n3) 复杂度,面对 1w 条的强测数据,查询时计算是不可取的,应该动态维护。维护方式是:每加入一个 A B 关系,就查询 A B 之间的共同熟人,triple_sum 则加上共同熟人的个数。细节:查找共同熟人的时候遍历熟人较少的那个人的熟人。

第十次作业

比较麻烦的是 modify_relation 后的 query_block_sum 。平时查到的并查集算法是解决已确定所有点和边的情形,没有考虑删边。QQ 水群里(应该最开始是 tyjj)提出了并查集分裂的改进法:删除 AB 边,则用 bfs 遍历与 A 有直接或间接关系的点,如果没有遍历到 B,那么将并查集分裂为分别以 A 和 B 为根的二层的两个并查集。相关代码如下:

private void delBlock(int id1, int id2) {
    HashSet<Integer> circled1 = new HashSet<>();
    ((MyPerson) people.get(id1)).setCircled(circled1);
    if (!circled1.contains(id2)) {
        int circleSize = 0;
        for (Integer pid : circled1) {
            pres.put(pid, id1);
            circleSize += 1;
        }
        preSize.put(id1, circleSize);

        HashSet<Integer> circled2 = new HashSet<>();
        circleSize = 0;
        ((MyPerson) people.get(id2)).setCircled(circled2);
        for (Integer pid : circled2) {
            pres.put(pid, id2);
            circleSize += 1;
        }
        preSize.put(id2, circleSize);

        blockSum++;
    }
}

其中 pres 是 ID → 并查集树上该点父节点的 ID 的 hashMap 映射;setCircled() 方法是 bfs 方法,执行后里面存储所有可达点的 ID。

现在回想起来,虽然说维护并查集用到的 bfs 只是在一个并查集小圈子里使用,而非整个 people ,但说不定不用维护并查集,直接在 qbs 时用 bfs 的复杂度也不算很高?

第十一次作业

挺折腾人的,query_least_moments 查询过指定源点的最小环长度。由复杂性可以排除动态维护的思想。

单次查询时,我先用 dijkstra 查找所有点到源点的最短路径,并在这个过程中维护最短路径生成树;接下来,用这篇博客提到的算法,分两类进行最小环查询。设整棵树为 0 级子树,往下的子树分别为 1,2,3 级子树。

  1. 遍历一级子树下的所有点(不包括一级子树根节点),如果这个点与源点有直接路径,则用 该点到源点的最短路径+该点与源点的直接路径 就能得到一个环的长度,依此更新最小环。
  2. 遍历一级子树下的所有点(包括一级子树根节点),如果被遍历点 A 与另一棵一级子树上的点 B 直接相连,则用 A 到源点的最短路径+ B 到源点的最短路径+ AB 之间直接路径 得到一个环的长度,依此更新最小环。

通过上面两类遍历后,就能得到最小环长度;如果最小环长度为初始值,说明没有找到环。为什么这样我也不知道,希望有 dalao 能发篇博客引引流。

更进化的 dijkstra 算法是堆优化,能够大幅度优化稀疏图的查询速度,对稠密图的速度似乎也会有提升,不过经过大量本地测试后认为不会超时,就没有继续优化。互测房里有见到用 Pair , ArrayQueue , TreeSet 等数据结构的,稀疏图速度是我没有堆优化的三倍以上。

规格与实现:性能与正确性

由于代码是看到群u的讨论结果后再下手,因此性能方面没有出现问题。关于性能的改进,除了动态维护算法的选择,还有一点:遍历时遍历对象的选取。例如第九次作业动态维护 triple_sum 时,遍历熟人较少的那个点,而不是随机选两人中的一人,也不是遍历 people 容器中的所有点。再例如第十一次作业的第二类最短路径树遍历时,可以选取源点的 value 容器遍历。

规格与实现的分离

课程组给的 JML 规格往往假定容器是一个数组,然后描述一个函数的功能也是“暴力”描述:在数组容器中,取出或者查询一个指定元素都是通过 for 循环。这样就导致了一些功能的描述(例如 query_least_moments )会出现大于等于二/三层的 for 循环,导致超过 O(n2) 甚至 O(n3) 的复杂度。

程序编写者要做的,就是不带歧义地理解程序设计者的意图:这个函数要实现什么功能(也就是翻译成自然语言),哪些变量不能被修改或重赋值,等等。在这些约束下,编写者应当作出变通:根据需要选择容器(以及数据结构)(HashMap 查询 key 快,TreeSet 动态维护最值,ArrayList 保证 message 的顺序),根据函数实现复杂度额外实现需要的方法,或者额外实现一个辅助类等。

OK 测试体验

OK test 是让我们从测试员的角度看待规格,编写单元测试程序,对代码的单元功能进行测试,看是否严格符合 JML 规格。

这单元编写的 OK test 方法将被修改容器的数据和其他关键变量的数据,以及被测试方法的返回值传入该测试方法,在测试方法内完全按照 JML 规格检测数值正确性。

单元学习体会

经过本单元学习,我体验了程序设计者、编写者和测试者这三个身份,大概了解了 0 级 JML 语言描述,同时深深体会了编写程序过程中对各种异常情况的考虑也依赖于 JML 才能周到。不仅如此我也学到了自定义异常的使用,学到了图论的一些算法知识。但其实就收获而言,除了这单元的图论算法,我感到收获甚微。相较之下,第一单元应对表达式各层级结构要自主考虑 package 和 interface 结构带来的便利以及递归下降的灵活使用,第二单元的多线程更是打开新世界的大门,它们给我“面向对象”的感受也更深刻。

虽然如此,第三单元面向规格编程(其实 OK test 并没有被我用来测试)还是让我有了新的体验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值