BUAA-OO-U3-博客总结

曹晨旭22230610 学生 2024-05-16 12:07:40

一、测试过程

1. 黑箱测试与白箱测试

  • 黑箱测试:是从一种从软件外部对软件实施的测试,也称功能测试或基于规格说明的测试。其基本观点是:任何程序都可以看作是从输入定义域到输出值域的映射,这种观点将被测程序看作一个打不开的黑盒,黑盒里面的内容(实现)是完全不知道的,只知道软件要做什么。因无法看到盒子中的内容,所以不知道软件是如何实现的,也不关心黑盒里面的结构,只关心软件的输入数据和输出结果。

    黑箱测试主要关注以下几个方面:

    • 功能是否正确:系统是否按照预期进行操作。
    • 异常和边界条件:系统在异常和边界条件下的行为。
    • 性能测试:系统在高负载和压力条件下的表现。
  • 白箱测试:也称结构测试或逻辑驱动测试,它是知道产品内部工作过程,可通过测试来检测产品内部动作是否按照规格说明书的规定正常进行,按照程序内部的结构测试程序,检验程序中的每条通路是否都有能按预定要求正确工作,而不顾它的功能。

    白箱测试主要关注以下几个方面:

    • 代码覆盖率:所有可能的应用程序路径(包括循环和条件语句)是否都被测试。
    • 数据流:数据在应用程序中的流动是否正确。
    • 控制流:应用程序的控制结构(如决策和循环)是否按预期工作。

2. 各种测试类型

  • 单元测试(Unit Testing)

单元测试通常是开发过程中的第一步,主要用于验证每个单元的功能是否正确。开发人员在编写代码的同时或之后进行单元测试,即对软件中的最小可测试单元进行检查和验证(在面向对象编程中通常是类或方法),以确保代码的各个部分(或“单元”)按预期工作。

  • 集成测试(Integration Testing)

当所有的单元测试通过后,接下来就是集成测试。在这个阶段,各个单元会被组合在一起进行测试,作为一个整体进行测试,以帮助发现单元之间的接口问题。

  • 功能测试(Functional Testing)

功能测试通常在集成测试之后进行,是一种黑箱测试方法,它关注的是软件的功能需求。功能测试确保软件的所有功能都按照预期工作。

  • 压力测试(Stress Testing)

压力测试通常在功能测试之后进行,这是一种性能测试,用于确定系统在高负载或压力条件下的行为。它可以帮助识别系统的弱点和限制,用于测试系统在极限条件下的性能和稳定性。

  • 回归测试(Regression Testing)

最后,每当软件的代码发生变化(例如,修复了一个错误,或者添加了一个新的功能),都需要进行回归测试。回归测试确保这些更改没有引入新的错误,或者使旧的错误再次出现。

3. 数据构造策略

  • 随机生成
  • 控制最大人数以及最多关系数,以控制图的稠密度
  • 考虑边界条件,如k5完全图、int类型最大值与最小值等

二、架构设计

1. 主要架构

在给定JML的基础上架构并没有太多的自由发挥的空间,但在此基础上我的独特架构包括:

DisjointSet

并查集,封装成单独一个类,用于第一次作业的qci指令。

  • 数据结构

    private final HashMap<Integer, MyPerson> persons;
    private final HashMap<Integer, Integer> father; // 节点->父节点
    private final HashMap<Integer, Integer> rank; // 节点->秩
    private int blockSum;
    private final HashSet<Integer> color; // 用于删除边时dfs遍历染色
    
  • 基本方法

        private void merge(int id1, int id2) {
            int fa1 = findFather(id1);
            int fa2 = findFather(id2);
            if (fa1 == fa2 && id1 != id2) {
                return; //如果父节点相同,根本就没有必要合并
            }
            int rank1 = rank.get(id1);
            int rank2 = rank.get(id2);
            if (rank1 < rank2) {
                father.put(fa1, fa2); //把秩较小的节点合并到秩较大的节点上
            } else {
                if (rank1 == rank2) {
                    rank.put(fa1, rank1 + 1); //秩相同的话,随便合并一下
                }
                father.put(fa2, fa1);
            }
            blockSum--; //分支数减一
            if (blockSum < 0) {
                System.err.println("error: blockSum is negative");
            }
        }
    
        private int findFather(int id) {
            int rep = id; //一开始代表元素为本身
            while (rep != father.get(rep)) { //循环获得id所在分支的代表元素
                rep = father.get(rep); //令rep迭代至顶层
            }
            int now = id;
            while (now != rep) { //压缩路径,并将新对应关系放入fa中
                int fa = father.get(now);
                father.put(now, rep);
                now = fa;
            }
            return rep;
        }
    

    为并查集最基本的两个方法。

    注意:删除边的时候需要进行dfs遍历:

        public void addRelation(int id1, int id2) {
            if (father.containsKey(id1) && father.containsKey(id2)) {
                merge(id1, id2);
            }
        }
    
        public void subRelation(int id1, int id2) {
            father.put(id2, id2);
            color.clear();
            dfs(id1, id1);
            if (father.get(id2) != id1) {
                blockSum++;
                color.clear();
                dfs(id2, id2);
            }
        }
    
        private void dfs(int id0, int id) {
            color.add(id);
            for (Integer o : persons.get(id).getAcquaintances().keySet()) {
                if (!color.contains(o)) {
                    dfs(id0, o);
                }
            }
            father.put(id, id0);
        }
    

    最后查找两个id是否处于同一block的时候还需要进行查找父节点:

        public boolean isSameBlock(int id1, int id2) {
            if (id1 == id2) {
                return true;
            }
            return Objects.equals(findFather(id1), findFather(id2));
        }
    

Triple

单独维护triple的类,在关系改变的时候维护它。

    public void addRelation(MyPerson person1, MyPerson person2) {
        HashSet<Integer> sameAcquaintance = new HashSet<>(person1.getAcquaintances().keySet());
        sameAcquaintance.retainAll(person2.getAcquaintances().keySet());
        tripleSum += sameAcquaintance.size();
    }

subRelation同理。

ExceptionCounter

用于计算异常数量的计数器类,实例化的时候要加上static关键词。

2. 图模型构建

主要采用HashMap类型,即由id映射到单个对象,例如位于MyNetWork中的persons、messages等。

3. 维护策略

三次作业中很多地方都用到了动态维护策略,例如计算valueSum的时候,在一个Tag中加入一个人就要改变valueSum的值,同理删掉人的时候也要减去相应的值。而在删掉关系的时候,需要遍历二者的共同好友的所有Tag,在这些Tag中维护valueSum。

三、性能问题

  • 第一次作业中用到的算法为并查集

  • 第二次作业中用到的算法为bfs

  • 第三次作业中没用到什么特别的算法

  • 此外,三次作业中都需要进行动态维护一些值

  • 规格和实现分离是一种设计原则,它强调将问题的描述(规格)与解决问题的方法(实现)进行分离。这种原则有以下几个优点:

    1. 灵活性:相同的问题可以采用不同的算法来解决,它们可能有不同的性能。
    2. 可维护性:当需要修改实现时,只需要关注实现部分,而不需要改动规格部分。
    3. 可扩展性:在理解规格的基础上,可以通过添加新的实现来扩展系统的功能。

    在面向对象设计中,规格通常体现在接口或抽象类或JML中,而实现则体现在实现这些接口或抽象类的具体类中。这样,只要新的实现满足原有的规格,就可以无缝地替换旧的实现,从而提高了代码的可维护性和可扩展性。这也是许多设计模式(如策略模式、工厂模式等)的基础思想。

    在本单元中,JML以及抽象的接口定义了代码的规范,而具体的实现则可以由程序员进行自主实现,在这其中就会涉及到不同的算法导致不同的性能的问题。

四、Junit测试

  • 尽量将测试方法与JML一一对应
  • 可能会需要用到影子容器,来检验调用方法前后的变化
  • 对于pure方法,要注意检验不能有异常抛出
  • 检验时要使用junit自带的方法(如assertEquals),而不要使用assert

五、体会感悟

JML是一种较为规范的描述语言,但是同时也存在着传递信息效率不高的弊端。但是在一些对精度要求高的项目中JML肯定能发挥重要的作用。总体来讲本单元较为轻松。

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

301

社区成员

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

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