BUAA-2024-OO-第三单元

杨昊天-21373026 2024-05-16 00:53:22

测试过程

黑箱测试与白箱测试

黑箱测试指的是测试人员在无法得知有关于代码的信息的情况下,通过输入和输出结果来进行测试,这样的测试的核心在于测试代码的功能性。

白箱测试指的是测试人员在可以获取代码的情况下,依据代码设计测试样例进行测试,这样的测试的核心在于测试代码的逻辑是否正确。

在面向对象编程的过程中,中测、强测是一种黑箱测试,互测是一种白箱测试,同时自己输入数据debug的过程也是白箱测试。

另外,JML的理念也类似于白箱测试,对代码的具体的方法进行功能性的规定以保证类内部的逻辑的正确性。

对单元测试、功能测试、集成测试、压力测试、回归测试的理解

单元测试:基于代码的封装性,对封装的单元进行测试。在第三单元的作业中可以表现为类内方法是否符合规格。

功能测试:对方法的功能进行测试,我们编写的MyNetworkTest就是对MyNetwork某个具体的方法进行的功能测试。

集成测试:尽管代码具有一定的封装性,但是封装得到的模块最后需要集成在一起实现最终的功能,集成测试可以在单元测试之后进行,测试集成的过程是否有误,也可以逐步进行以便于debug。

压力测试:压力测试包括程序在极端情况下的稳定性测试以及在大量数据情况下的性能测试。本单元作业里,isCircle、modifyRelation、queryCoupleSum、queryBlockSum、queryTagValueSum、queryTagAgeSum、queryTagAgeVar、queryBestAcquaintance、queryShortestPath等就是性能测试的主要对象。

回归测试:在代码修改之后,测试代码修改之前正常的功能是否仍然正常的测试。一般用于bug修复合并的时候进行测试。

数据构造的策略

一个比较好的数据构造策略可能是大量随机数据与少量特殊数据相结合的方式。

譬如,随机构建的数据可能很难实现全连接图这种极端数据,可以通过加入特殊数据的方式构建。

但是实际操作中,课程组没有给出极端情况下才会出现的bug,因而纯随机构造策略也通过了测试。

具体来说,每次数据构造(对应测试的@Parameters方法)都会得到一些到达运行待测试方法前的网络状态所需的指令。

譬如,我们测试queryCoupleSum方法,对此方法有影响的主要是person和relation,所以我们的构造数据只需要一系列的addPerson和addRelation即可。

为什么我们不需要modifyRelation、deletePerson呢?因为在除了queryCoupleSum方法之外的方法全部正确的前提下,仅通过addPerson和addRelation我们就可以构造任意结构的网络,为queryCoupleSum提供足够多样化的测试环境。

如果我们想要测试更复杂的方法,比如deleteColdEmoji,这个方法会涉及Message,那我们就需要构造一些addMessage和sendMessage指令,但是我们并不需要clearNoticeMessage指令,其原因也是同理。

本单元的架构设计/性能问题及其修复情况

实际上,从JML的角度来说,JML和interface已经完成了架构设计,此处主要阐述每次作业的具体实现。性能问题与修复情况会在每次作业的bug部分进行讲解。

第一次作业

题目分析

题目要求继承异常类、Network类和Person类,并实现JML描述的类的方法。

实现方案

在规格与设计的方面:在JML里,people以数组的形式描述,但是考虑到每一个person都有独立的id,且往往需要根据id获取person对象,我们用HashMap存储people数组;Person类内部JML也以数组的形式描述acquaintance,但是同样的原因我们也用HashMap存储acquaintance。

在算法的方面:Network的queryTripleSum、queryBlockSum和isCircle是可能耗时最多的。我们考虑通过用并查集的方式来优化isCircle的查询,在addRelation的时候更新并查集;同时考虑到queryTripleSum和queryBlockSum都是针对整个NetWork的,我们可以存储这两个函数的值为qts和qbs,并动态更新,在查询的时候直接返回,qts需要在addRelation的时候更新;qbs需要在addRelation和addPerson的时候更新。

我们具体分析以上三个方法,queryTripleSum如果在检索时计算,复杂度为n^3(遍历三元组),每一次更新的时候维护,复杂度为n(增添两人的关系,只要遍历所有人看是否有人同时与这两人link即可);queryBlockSum,在检索时计算的复杂度为n(以广度优先搜索的方式划分block),每一次更新的时候维护,复杂度为1(如果增添关系之前,两个人不在同一个block,那么block数量会减1);isCircle,检索时计算的复杂度为n(bfs),每一次更新的时候维护,复杂度为1(并查集更新父结点)。

由此可见,以上方法相比"检索时计算"和"更新时维护",复杂度上具有显著差异。

bug

第一次作业没有出现bug

第二次作业

题目分析

本次作业加入了tag类,在tag内可能查询ageVar valueSum;在tag之外,Network加入了queryBestAcquaintance queryShortestPath queryCoupleSum和modifyRelation。

实现方案

第二次作业加入了Tag,Tag方面会加入人和删去人,并随时可能查询:内部关系价值总和以及年龄方差;对Person可能需要记录关系价值最高的对象的id(bestId);Network需要快速查询相互为bestId的对数:coupleSum和两人之间link图的最短路径。

在规格与设计层面,在Tag内部为了加快内部关系价值总和以及年龄方差,我们同样尝试将这几个值记录下来:ageAvr、valueSum并动态更新,在向组内添加或删除组员的时候,对valueSum的更新是很快的,但是ageVar几乎需要重算一遍,因此我们每次都记录ageVar为上一次查询的值并增添其脏位,并在组内添加或删除组员的人标记其脏位,当存在干净的上一次查询的值的时候我们直接返回ageVar,否则重新计算。

对Person类需要知道bestId,有的同学可能采用堆排序,但是实际上,堆排序会将每次增添/删除acquaintance的成本从O(1)增加到O(logn)。如果我们用另一种方法,仍然用普通的列表记录acquaintance,但是记录bestAcquaintace的Id bestId和bestId对应的value bestValue,那么在以下情况下可能更新bestId:新增关系,value>bestValue;modifyRelation,变化后的value>bestValue;删除关系,删除的acquaintace正是bestId。这两种方法,前者增添了每一次增删的成本,但是增添得不多,后者的动态维护有的时候是O(1)复杂度,有的时候是O(n),但是增删成本为O(1)。因此是否使用堆来对acquaintance进行管理并不能立刻判断,经过和其它同学的讨论、调研,我们发现,堆排序不一定比普通的列表记录+动态维护bestId更快速。

算法层面上,本次作业的一个难点实际上是modifyRelation,这个操作可能会导致关系的删除或价值的变化,关系的价值变化删除可能影响我们记录的并查集、qbs和qts、person的bestId,group的valueSum,尤其是删除关系后对并查集的更新需要着重考虑。我们考虑在删除关系之后,以被删除的关系的两边分别作为起点进行宽度优先搜索更新块号。另一个不算难的地方是查询最短路径,本次我采用的bfs,但是双向bfs显然能进一步优化效率。

bug

本次作业的bug主要是压力测试,有测试点将network的人数增加到几千个并多次进行queryCoupleSum导致超时。

为什么会超时呢?主要之前考虑到,已经将queryBestAcquaintance的复杂度降低到O(1)了,queryCoupleSum每次查询也就是O(n^2)的复杂度。

但是这个复杂度累计到n为几千的时候就很高了。

实际上按照我们的方法进行分析,如果我们每次add relation和modify relation动态维护coupleSum的值,由于我们已经实现了动态维护bestId,所以这个维护的复杂度完全就是O(1),当时没做coupleSum的动态维护唯一理由就是懒了。(当你modify一个relation的时候,其可能涉及的 会导致coupleSum变化 的情况,需要分类讨论,略显繁琐)。

比如以下是我更新coupleSum的代码:p1BestBefore和p2BestBofore代表person1 person2在改变关系之前的bestId,p1CoupleBefore和p2CoupleBefore代表person1 person2在改变关系之前是否与其bestId组成couple,p1CoupleNow和p2CoupleNow代表person1和person2在改变关系后是否与其bestId组成couple。

public static int updateQcs(MyPerson person1, MyPerson person2,
                                Integer p1BestBefore, Integer p2BestBefore,
                                boolean p1CoupleBefore, boolean p2CoupleBefore,
                                boolean p1CoupleNow, boolean p2CoupleNow)
    {
        int id1 = person1.getId();
        int id2 = person2.getId();
        Integer p1BestNow = person1.getBestIdNoExp();
        Integer p2BestNow = person2.getBestIdNoExp();
        int ret = 0;
        boolean coupleBefore = Objects.equals(p1BestBefore, id2) &&
                Objects.equals(p2BestBefore, id1);
        boolean coupleNow = Objects.equals(p1BestNow, id2) &&
                Objects.equals(p2BestNow, id1);
        if (coupleBefore) {
            if (coupleNow) {
                return ret;
            }
            else {
                ret--;
                if (p1CoupleNow) {
                    ret++;
                }
                if (p2CoupleNow) {
                    ret++;
                }
            }
        }
        else {
            if (coupleNow) {
                ret++;
                if (p1CoupleBefore) {
                    ret--;
                }
                if (p2CoupleBefore) {
                    ret--;
                }
            }
            else {
                if (!Objects.equals(p1BestBefore, p1BestNow)) {
                    if (p1CoupleBefore) {
                        ret--;
                    }
                    if (p1CoupleNow) {
                        ret++;
                    }
                }
                if (!Objects.equals(p2BestBefore, p2BestNow)) {
                    if (p2CoupleBefore) {
                        ret--;
                    }
                    if (p2CoupleNow) {
                        ret++;
                    }
                }
            }
        }
        return ret;
    }

第三次作业

第三次作业主要加入了Message,Message有socialValue,会改变发信人、收信人的socialValue;在Message的基础增删、发送之外,network新增了queryPopularity、clearNoticeMessage、deleteColdEmoji、storeEmojiId。

实现方案

senMessage向Person内部加入message,并且时常需要查询最后加入的若干个message,我们考虑用LinkedList来存储Person内部的messages。

而本次作业反而没有太多需要优化运算速度的地方,有关消息的部分大多数都是增删消息和直接查询。

bug

目前bug在发送红包消息的时候出现了除以零的错误,还未debug。

规格与实现的分离

具体来说,规格的本质是提出需求者将自然语言转化为形式语言描述;而实现需求者并不直接对照JML进行编程,而是自行理解整个模块的JML(理解整个模块的功能,转化为自然语言),再进行模块的编写,将自然语言转化为代码。

因而规格的本质是避免自然语言的语义不清晰问题,而且便于我们进行单元测试。

具体来说,有五类方法,可以在既定规格下优化实现:

第一类是数据结构,JML采用数组的方式描述数据,我们在具体实现中可以采用HashMap,LinkedList等,来满足基于id查询、删除前几个这种需求。

第二类算法,比如最短路径查询,我们可以使用bfs 双向bfs等实现。

第三类是动态维护,在增删数据的时候对某些值进行动态更新,比如coupleSum,tripleSum。

第四类是延迟维护,比如对ageVar设置脏位。

第五类是数学优化,比如优化平方差的计算方式。

JUnit测试

Junit主要分为两部分,数据准备和结果验证。

数据准备我们在前面在"数据构造的策略"已经有所提及,在我们已知除了待测试方法以外的方法都正确的情况下,我们并不需要使用所有方法才能构造测试环境,比如构造一个网络,我们只需要add person和add relation,而不需要remove person和modify relation。

结果验证的部分尽可能完全依照JML进行,比如遇到\exists语句,首先设置boolean exists = false;然后遍历,遇到符号条件的情况设置exists=true再break,在循环末尾assertTrue(exists)。

JUnit的验证能力还是很强的,通过随机数据生成100组,能够覆盖绝大多数情况,而且和JML对应。

但是这一部分会有一个问题,就是无限验证,比如queryShortestPath中,要求找到的Path比所有Path都短,但是如何枚举所有的Path呢?如果所有的Path尚可枚举,假如我的规格是返回最小的正数,如何枚举所有的正数呢?这一点我并不明白。

本单元学习体会

在规格设计单元最重要的体会应该就是规格与设计的分离,规格是对方法的功能性要求,我们在本单元并不需要集成,因此可以说几乎所有bug都是设计不符合规格或者设计本身性能较差引发的。

规格本身的描述性决定了很多的方法不能照搬规格(否则算法复杂度将很高),诸如选择合理的数据结构、记录值而后动态更新、记录上一次查询的值并设定脏位、算法优化等都是必要的。

...全文
59 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧