BUAA-OO-24-Unit3总结

孙延范-22371521 学生 2024-05-16 11:58:53

第三单元总结


1 本单元的测试过程

1.1 黑箱测试、白箱测试的理解

黑箱测试

黑箱测试是一种功能测试,它不关心软件内部的结构和逻辑,只关注软件的输入和输出。测试人员将软件视为一个“黑箱”,只通过规定的输入来观察输出的结果,以此来判断软件的功能是否符合需求规格。黑箱测试的主要目的是验证软件的功能性需求,确保所有用户功能都能按照规格说明正确执行。在第三单元的测试中,我使用了其他同学开源的评测机,与其他人对拍来检测正确性,不过仍有一些bug没有查出。

黑箱测试的特点包括:

  • 测试人员不需要了解程序内部逻辑。
  • 测试用例基于软件需求和规格说明。
  • 适合于高层接口和输入输出数据的测试。
  • 不能完全覆盖所有的执行路径。

白箱测试

白箱测试,又称为结构测试或透明箱测试,是一种测试方法,它基于对软件内部结构、设计和代码的详细了解。测试人员利用程序的内部逻辑结构及相关信息来设计测试用例,检查程序中的逻辑路径是否都按预定的要求正确执行。在第三单元的测试中,通过人脑遍历可能的情况,针对代码实现和功能来构造数据测试,但是容易局限于个人的思维。

白箱测试的特点包括:

  • 测试人员需要了解程序的内部逻辑和结构。
  • 测试用例基于代码的结构和控制流。
  • 能够检查程序内部操作的正确性。
  • 可以检测代码中的隐藏错误和潜在的漏洞。

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

单元测试

单元测试是针对软件中最小的可测试部分(通常是函数或方法)进行的测试,目的是验证一个单元是否按照预定的设计和规范正常工作。这几次的Junit测试其实就是单元测试的一种方法。

功能测试

功能测试是针对软件的特定功能或一组功能进行的测试,以确保它们满足用户需求。这种测试通常是基于软件的需求规格说明,不涉及内部结构和设计。第三单元针对单个指令的正确性进行测试就属于功能测试,他们通常涉及几个方法的协作。

集成测试

集成测试是在单元测试之后进行的,它测试多个组件或系统模块组合在一起时的运行情况。集成测试的目的是检查不同模块之间的接口和交互是否符合设计要求,确保它们能够作为一个整体正常工作。第三单元中对于多个指令的正确性进行测试就属于集成测试,比如加一个message再发送出去,测试是否能够正确的发送。

压力测试

压力测试是一种性能测试,它评估软件在极端工作负载下的稳定性和错误处理能力。这种测试通过模拟高负载、大量数据或其他极端条件来检查软件是否能够正常响应,以及它的性能和稳定性如何。第三单元中,压力测试十分重要,因为强测的数据量很大,简单根据JML实现的方法由于时间复杂度太大,很容易CTLE,就需要优化。

回归测试

回归测试是在软件发生更改(如添加新功能、修复错误或进行优化)后进行的测试,以确保这些更改没有引入新的错误,并且原有功能仍然正常工作。回归测试的目的是确保软件的修改没有对现有功能产生不利影响,防止被以往的指令偷家。

1.3 数据构造

手动构造数据,针对想到的所有情况设计。

  • 可以构造极端数据,防止某些方法耗时过多,出现TLE
  • 可以构造边界数据,测试一些情况下是否会发生异常。
  • 可以构造大数据,随机生成指令,大概率能测试到所有的情况。

2 架构设计

2.1 图模型构建

hw9 中isCircle如果采用dfs,且没有动态维护,那么queryBlockSum就会超时,被强测爆了之后,痛定思痛,把能做的优化都做了。

并查集记录连通图,并做了路径压缩按秩合并的优化。

  • 并查集

    并查集是一种数据结构,用于处理一些不交集的合并及查询问题。

    private final HashMap<Integer, Integer> parent = new HashMap<>();
    private final HashMap<Integer, Integer> rank = new HashMap<>();
    

    用一个HashMap记录每个节点的父节点,当两个节点合并时,将其中一个节点的父节点指向另一个节点。
    再用一个HashMap记录每个节点的秩,即以该节点为根的树的高度。

  • 路径压缩

    路径压缩是在查找根节点的过程中,将路径上的所有节点的父节点都指向根节点,这样可以减小树的高度,提高查找效率。

    private int find(int x) {
        if (parent.get(x) != x) {
            parent.put(x, find(parent.get(x)));
        }
        return parent.get(x);
    }
    
  • 按秩合并

    按秩合并是在合并两个节点时,将秩较小的树合并到秩较大的树上,这样可以减小树的高度,提高查找效率。

    private void union(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);
        if (rootX != rootY) {
            int rankX = rank.get(rootX);
            int rankY = rank.get(rootY);
            if (rankX < rankY) {
                parent.put(rootX, rootY);
            } else if (rankX > rankY) {
                parent.put(rootY, rootX);
            } else {
                parent.put(rootY, rootX);
                rank.put(rootX, rankX + 1);
            }
        }
    }
    

2.2 图维护策略

在并查集的维护中,合并操作只需要我们比较两者的父节点,若不一致就可以合并,否则它们已经在一个集合里了。比较难受的就是关系有可能会删除,而并查集并不太适合删除操作,删除操作需要更新图。

public int split(int id1, int id2, HashMap<Integer, Person> idMap) {
    ArrayList<Integer> visited = new ArrayList<>();
    if (dfs(id1, id2, visited, idMap)) {
        return -1;
    }
    for (Integer id : visited) {
        parent.put(id, id1);
        rank.put(id, 0);
    }
    visited.clear();
    dfs(id2, id1, visited, idMap);
    for (Integer id : visited) {
        parent.put(id, id2);
        rank.put(id, 0);
    }
    return 0;
}

所以在删除关系时,我们采用了dfs的方法,如果两者仍然连接,就说明它们还在一个集合,否则就将它们分开,设置新的父节点。

3 性能问题及其修复情况和规格和实现分离

我的性能问题主要出现在hw9,使用动态维护和并查集解决了性能问题,并查集具体情况见架构设计。

此外还有一些优化性能的方法:

动态维护

本次作业中,很重要的一个优化方法就是动态维护。在queryBlockSumqueryTripleSumqueryTagValueSumqueryTagAgeVar等指令中,我们需要在每次查询时都重新计算一次,这样会导致时间复杂度过高。所以我们可以在每次修改关系时,更新一下这些值,这样在查询时就不需要重新计算了。

例如queryBlockSum可以维护一个blockSum,加入person的时候相当于新加入了一个block,合并并查集的时候blockSum减一,分开一个并查集的时候blockSum加一。

数据结构

虽然JML中基本上都是用数组来描述的,但是我们在实际编程中并不一定要使用数组,我们可以根据实际情况选择合适的数据结构,比如HashMapLinkedListPriorityQueue等。

公式优化

qtav指令中,我们需要计算年龄的方差,但是没必要根据JML中的算法来计算,而是可以维护agePowSumageSum替换成下面的代码:

int ageMean = getAgeMean();
long tmp = agePowSum - 2L * ageMean * ageSum + (long) ageMean * ageMean * idMap.size();
return (int) (tmp / idMap.size());

这样就可以降低时间复杂度。

规格与实现分离

在开发中,规格通常包括需求文档、设计文档、接口定义等,它们定义了软件系统的功能、性能、接口和其他非功能性需求。实现则是具体的代码,包括算法、数据结构、模块交互等。

规格与实现分离是一种软件工程的最佳实践,它强调在设计和开发软件系统时,应该明确区分系统的规格说明(也就是“应该做什么”)和系统的具体实现(也就是“如何做”)。这种做法的核心思想是将抽象层和具体实现层分开,以增强系统的灵活性、可维护性和可扩展性。

打个形象的比喻,规格就像是一份菜谱,它详细描述了做一道菜需要哪些材料、步骤和烹饪方法;而实现则是实际炒菜的过程,包括切菜、炒制等具体动作。一个好的厨师会严格按照菜谱来烹饪,保证菜品的质量和口味。同样,一个好的软件开发者会根据规格来编写代码,确保软件的功能和性能符合预期。

4 Junit测试

4.1 与规格信息相结合

我的Junit测试随机生成两个一样的Network实例,然后其中一个采用翻译JML得到的标准代码计算,另一个调用对应的方法,然后对比两者是否一致。

至于对比什么,就根据规格信息中的assignable,ensures的要求来对比,为了覆盖到所有ensures的要求,需要修改随机生成的代码,使得能够覆盖到所有情况。

4.2 Junit检验代码实现与规格一致性的效果

Junit对于代码实现与规格的一致性的检验还是很有效果的,甚至在hw11中帮我找到一个bug,不过Junit测试的代码量也不小,需要花费一定的时间来编写,对所有的指令进行覆盖也是一个挑战,而且如果理解JML有误的话,那从根本上就是错的。出于效率上的考虑,我就没有对要求以外的方法做Junit测试。

学习体会

在学习了这个单元之后,我深刻感受到了规格化编程的魅力。虽然有些前辈对JML在实际开发中的应用持保留态度,但我认为,每一项技术的存在都有其独特的价值。JML作为一种规格化语言,它帮助我们更清晰地理解程序的行为,确保代码的可靠性。
这单元的学习,就像是一场对细节的严格考究。JML的精确规格,让我在编写代码时不得不放慢脚步,仔细思考每一行代码的含义和可能的影响。这种细致入微的工作方式,虽然有时候让人感觉繁琐,但却是确保高质量代码的关键。
而且,我发现JML不仅仅是一种验证工具,它还是一种思维方式。它教会我如何从更高的层面去理解和设计系统,如何在代码实现之前就预见到可能出现的问题。这种前瞻性的思考,对于开发大型和复杂的系统尤为重要。
当然,JML并不是孤立存在的。在实际的开发中,我们还需要考虑算法的效率,优化性能。这单元的练习,也让我有机会将理论应用到实践中,尝试着在满足规格的同时,提升代码的性能。
回顾这段学习经历,我感觉自己不仅学会了JML的使用,更重要的是,我学会了如何用规格化的思维去设计和编写代码。这种思维方式,我相信会在未来的编程之路上,成为我宝贵的财富。

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

301

社区成员

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

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