BUAA_OO_Unit3总结

furina的动物朋友 学生 2024-05-16 23:08:15

1. 本单元的测试过程

1.1 黑箱与白箱测试

黑箱测试

黑箱测试是一种功能性测试方法,测试人员不需要了解被测试软件的内部结构和实现细节,只需要关注软件的输入和输出,以及软件对于不同输入的响应是否符合预期。测试案例通常基于需求规格说明书或用户文档,而不考虑软件的实现细节。黑箱测试着重于测试软件的功能、性能、安全性等方面,以确保软件能够按照预期的方式工作。

白箱测试

白箱测试是一种结构性测试方法,测试人员需要了解被测试软件的内部结构、代码逻辑和数据结构,并根据代码的内部逻辑设计测试用例,以确保代码的每一条路径都被覆盖到。白箱测试通常包括代码审查、静态分析、单元测试、集成测试等技术手段。白箱测试着重于测试软件的内部逻辑是否正确、代码是否规范、是否存在潜在的错误等方面。

1.2

单元测试

是针对软件中的最小可测试单元进行的测试,通常是函数、方法或类。目的是验证每个单元的功能是否按照预期工作,以及在修改代码后是否保持正确性。

功能测试

用来测试软件的功能是否按照规格说明书或用户需求的要求正常工作。这种测试关注的是软件的行为,而不是其内部结构。

集成测试

用来测试不同单元或模块之间的交互是否正确,以及它们在组合后是否按照预期工作。目的是检查各个单元之间的接口是否正确,数据是否正确传递,以及组合后的整体功能是否正常。

压力测试

压力测试是测试软件在极限条件下的性能和稳定性,例如高负载、大数据量或持续运行时间较长等情况。其目的是评估软件在不同压力下的表现,并找出可能的性能优化点或由于数据积累而产生的bug。

回归测试

回归测试是在对软件进行修改或添加新功能后,重新运行之前的测试用例,以确保修改不会对现有功能产生负面影响。它的目的是验证修改后的代码是否仍然符合原有的功能和性能要求,同时检测是否引入了新的错误。

1.3 数据构造策略

从局部到整体,先针对实现较为复杂的方法设计测试样例保证方法能够实现预期的功能随后设计综合测试数据来测试程序运行时方法之间的协作是否正确。当然,由于本单元对性能要求较高,需要编写一些极端的样例进行测试,考察极端情况下程序是否能正确运行以及性能是否还存在优化空间。

2. 架构设计

2.0 图模型构建

笔者并没有另设计类或容器去存储记录本单元中的社交网络图,仅靠person类中要求实现的acquaintance以及value容器来实现这个图。值得注意的是,这个图对Network类并不透明,因此Network只能通过persons容器中的person来实现对社交网络图的修改和维护。

2.1 hw9

本次作业的结构简单而清晰。MainClass通过驱动Runner类调用Network来实现对于社交网络的模拟。Network中有成员Person作为社会网络中的节点,四个异常类针对方法中不符合规格的操作来抛出异常。

性能问题

本次作业的性能问题主要出现在isCircle连通性查询以及BlockSum和TripleSum的维护上。

2.1.1 并查集与连通性查询

若每次在调用isCircle方法时都使用广度或深度搜索方法的话一定会出现超时的问题。
所以我采用了并查集的算法:为每一个连通块选出一个成员作为根节点,为每一个person添加一个root属性来储存根节点的id(person的id是唯一的)。检查两个成员是否连通只需检查它们的root是否相等。

并查集的使用比较简单,其重点在于如何维护。导致并查集发生变化的行为无非只有两个:addRelation和modifyRelation。
在addRelation时要判断两个person的root是否相等,若不相等则说明两个person处于两个不同的连通块当中,这时就需要将两个连通块合并:将其中一个连通块的根节点的根节点设置为另一个连通块的根节点。

if (myPerson.getRoot(persons) != this.getRoot(persons)) {
    MyPerson personRoot = (MyPerson) persons.get(myPerson.getRoot(persons));
    personRoot.putRoot(this.getRoot(persons));
    MyNetwork.subBlockSum();
}

由于被合并的连通块中只有根节点的root被修改,因此在查询时需要将每个person的root进行维护来保障它们root的统一:

public int getRoot(HashMap<Integer, Person> people) {
    if (id != root) {
        MyPerson myPerson = (MyPerson) people.get(root);
        root = myPerson.getRoot(people);
    }
    return root;
}

modifyRelation则是一个比较麻烦的方法,因为修改关系后即使两个person间没了关系,但仍可能处于一个连通块当中。因此我选择使用深搜的方法来判断两个person是否还处于同一个连通块中:
将person1的root设置为它自己、将person2的root也设置为它自己,之后通过深搜遍历person1的连通块并将所有节点的root都设置为person1。进行完这一操作后查询person2的root是否为person1。若不是person1则对person2的连通块进行深搜并把所有节点的root设置为person2;若是则不需要进行任何操作。

HashSet<Integer> flag1 = new HashSet<>();
person1.putRoot(person1.getId());
person2.putRoot(person2.getId());
dfs(person1.getId(), person1.getRoot(persons), flag1);
if (person2.getRoot(persons) != person1.getRoot(persons)) {
    blockSum++;
    HashSet<Integer> flag2 = new HashSet<>();
    dfs(person2.getId(), person2.getRoot(persons), flag2);
}
2.1.2 BlockSum 与 TripleSum 的维护

BlockSum 的维护难度相对较低,在addPerson时由于person和其他成员没有任何关系,则进行addBlockSum操作;在addRelation时判断两个person是否属于同一连通块,若不属于则进行subBlockSum操作;在modifyRelation结束后若两个person不属于同一连通块则addBlockSum。

TripleSum 的复杂度较高,是一个比较耗时的方法,处理不当很容易导致tle。
1.缩小遍历规模
笔者最开始的计划是遍历persons中所有的person来寻找同时与两个person相连的成员、每找到一个则进行addTripleSum或是SubTripleSum操作。之后通过与同学交流发现可以遍历其中一个person的acquaintance,判断acquaintance中的成员是否与另一个person相连。
2.重建并查集时进行判断
在hw9强测结果公布后发现某些数据点中含有大量的mr指令。笔者原本的写法是mr每发生一次删边操作则进行一次并查集重建,这样在包含大量mr的数据中有可能会超时。在进行TripleSum是否发生改变的判断过程中,若发现person1的acquaintance中某个成员与person2相连,则说明即使删除了person1和person2之间的关系,person1和person2仍可以通过那个成员连通,这时并不需要进行并查集重建,因为person1和person2仍处于同一连通块中。

boolean subFlag = true;
for (Person person : person1.getAcquaintance().values()) {
    if (person.isLinked(persons.get(id2)) && person.getId() != person2.getId()) {
        subTripleSum();
        subFlag = false;
    }
}
if (subFlag) {
    rebuild(person1, person2);
}

2.2 hw10

和hw9相比多出了Tag(标签)类,可以将该类理解为person将和自己有关系的人进行了分类。Network和Person中也新增了一些方法和容器来实现Tag所需的功能。

性能问题

本次作业的主要性能问题出现在CoupleSum和ValueSum两个复杂度为O(n^2)的方法上。笔者在编写代码的过程中选择使用脏位来避免大量、连续地查询。

CoupleSum的维护方式比较简单,为myNetwork添加属性dirty并将其初始化为true。因为只有addRelation和modifyRelation两个方法会改变某个person的bestAcquaintance,所以只需要在调用两个和Relation相关的方法后将dirty置为true,每次查询CoupleSum时若dirty == true则按照jml的写法进行计算,并将dirty设置为false。

对于ValueSum的维护,笔者选择在Tag中加入脏位dirty来判断是否要维护ValueSum。ValueSum的维护方式则相对复杂,因为有很多操作都会改变某个Tag的ValueSum。这些方法有的位于Tag中,有的还位于Network中,而tag对于Network并不是透明的。因此我选择在myNetwork中加入容器tags,在addTag时将tag加入tags;在deleteTag时将tag从tags中删除。在进行addRelation或modifyRelation时只需要遍历所有的tags并将同时包含两个person的tag的脏位置为true。针对不同person的tag可能出现id相同的情况,我选择用HashMap来容器,key为String型,将personId + “ ” + tagId作为key。(笔者当时不想通过persons来遍历tags只是担心O(n^2)导致tle,不过现在想想差别好像也不大)

2.3 hw11

新增了Message类,其中有RedEnvelopeMessage、NoticeMessage以及EmojiMessage,主要用于Person之间的信息交互,发送不同的Message可以改变发送者和接收者的一些属性。Person中新增了容器messages以及一些新的方法和属性来实现接收和发送Message。

性能问题

本次作业性能问题少,需要注意的只有person中messages的容器选择(LinkList),因为有将message插入到messages首位的操作。

2.4 规格与实现分离

规格是标准,体现某个方法或类必须满足的要求。规格可以告诉我们怎样写能够满足正确性,但对性能和实现方式都没有限制。这使得开发者可以在满足要求的基础上去优化其性能,测试者只需根据规格编写测试程序检验实现是否正确。本单元作业中的诸多"维护"就是对规格与实现分离最好的体现。

3. Junit测试

本单元的Junit测试实现与规格“对答案”的过程:首先自己生成数据,将需要测试的方法原封不动按照其jml编写一个测试程序,之后将自己程序的运行结果与测试程序的运行结果通过断言来判断正误即可。

需要注意的是不光要检查@ensures 还要检查@pure,检验调用对象在调用方法前后不会发生改变,不然无法通过某些测试。

不能盲目生成数据,而是要在随机生成的基础上具有一定目的性。比如要同时检测完全图和稀疏图情境下方法的运行情况。

4. 学习体会

本单元接触了JML规格化编程,学习了契约式编程的思想。其学习强度要明显低于前两个单元,提前写好的JML让我们无需在构建架构上耗费大量时间。对于性能的要求为我们提供了一个机会来复习巩固曾经学过的算法。
JML使代码需要实现的功能变得非常明确、严谨,编写的过程也不需要加入很多自己的思考。但由于本地与评测机环境差异过大,本单元对于性能的要求让人有点摸不着头脑。同时,中测的强度较低(甚至不如样例),希望能够加强中测的强度,对每次作业新添加的功能进行诊断。

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

301

社区成员

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

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