北航面向对象第三单元总结

寒月倚南枝 学生 2024-05-16 22:43:42

分析本单元测试过程

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

黑箱测试(Black Box Testing): 黑箱测试是一种功能性的测试,测试人员不需要了解被测试的代码内部结构和工作原理,只需关注输入和输出是否符合预期。测试人员通过测试来检测软件的每个功能是否都能正常工作,而不关心软件内部的具体实现。黑箱测试通常用于系统测试、验收测试和 Regression 测试。

白箱测试(White Box Testing): 白箱测试是一种结构性的测试,测试人员需要了解被测试代码的内部结构和工作原理,基于代码的结构来设计测试用例。这种测试方法要求测试人员能够访问源代码,通过检查程序的逻辑路径、内部状态和结构来确保所有内部组件都得到了适当的测试。白箱测试通常用于单元测试和集成测试。

顾名思义,二者最重要的是测试人员是否了解被测试代码内部结构和工作原理,黑箱注重输入输出预期是否正确,白箱要求基于代码结构设计测试样例

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

单元测试(Unit Testing): 单元测试是对软件中的最小可测试单元进行检查和验证。在面向对象编程中,最小测试单元通常是方法或函数。我们使用的Junit就是单元测试,目的是确保每个部分都按预期工作,符合JML规格要求。

功能测试(Functional Testing): 功能测试验证软件的功能是否满足需求规格说明。这种测试可以是对单个功能或多个功能的测试,目的是确保软件的主要功能按照预期工作。

集成测试(Integration Testing): 集成测试是在单元测试之后进行的,它测试组合在一起的两个或多个组件是否能够作为一个整体正常工作。这种测试旨在发现组件之间接口的问题。

压力测试(Stress Testing): 压力测试是通过模拟高负载情况来测试软件的稳定性和性能。这种测试的目的是找出软件在极端条件下的弱点和瓶颈,确保软件能够在高负载情况下正常运行。强测采用的数据大多是压力测试,刻意使用高负荷来卡cpu运行时间,达到性能的检测。

回归测试(Regression Testing): 回归测试是在软件发生更改(如添加新功能或修复缺陷)后进行的,以确保更改没有引入新的错误,并且原有功能仍然正常工作。比如强测时检测之前的函数来检查之前迭代的功能是否发生改变。

数据构造有何策略

1.等价类划分:将输入数据的集合划分为若干等价类,从每个等价类中选取代表性的值作为测试用例。例如在第一次测试中,被测试函数是检查图模块的连通性,按照等价类划分,我分别生成了完全图,部分连通图以及孤立点图来进行测试,覆盖较为全面。

    for (int i = 0; i < testNum; i++) {
            MyNetwork network1;
            if (i % 3 == 0) {
                if (i < 15) {
                    network1 = generateAlone();
                } else {
                    network1 = generateLoop();
                }
            } else if (i % 3 == 1) {
                network1 = generatePart();
            } else {
                network1 = generateAll();
            }
            object[i] = new Object[]{network1};
    }

2.边界值分析:选取正好等于、刚刚大于或刚刚小于边界的值作为测试数据。例如第三次测试当中,测试包含一个参数limit,要注意选取limit=0和limit=MAX来进行边界值的覆盖,类似的,数据类型也应当进行边界值覆盖,比如int型变量的边界值。

3.错误猜测:基于经验和直觉推测程序中可能存在的错误,设计测试用例来暴露这些错误。 主要还是依赖于assert模块,按照JML规格进行检查。

4.因果图:通过分析输入与输出之间的因果关系来设计测试用例。 比如第二次测试当中我生成了一个特殊的循环链图Loop,调整不同边权重,确保这样输出的bestCouple数量一定为0来预测输出结果。

本单元的架构设计,分析图模型构建和维护策略

第一次作业

第一次作业并未使用并查集,使用了HashMap作为图的存储结构,并使用 Value记录边权重。

在连通性检查isCircle当中采用了dfs深度优先搜索的方法,遍历节点来检测连通性,复杂度不高

    public boolean dfs(int id) {
        flag = false;
        if (this.id == id) {
            return true;
        }
        for (Map.Entry<Integer, Person> entry : acquaintance.entrySet()) {
            MyPerson myPerson = (MyPerson)entry.getValue();
            if (myPerson.flag) {
                if (myPerson.dfs(id)) {
                    return true;
                }
            }
        }
        return false;
    }

在连通块数量查询queryBlockSum当中采用了dfs深度优先搜索方法,遍历所有结点一次来实现连通块数量记录

    public void setBlockSum() {
        setFlags();
        this.blockSum = 0;
        for (Person person : personArrayList) {
            if (((MyPerson) person).getFlag()) {
                ((MyPerson) person).dfsBlock();
                blockSum++;
            }
        }
        setFlags();
    }

在三角形数量查询queryTripleSum时采用了维护中间量的方式,在增加关系和删除关系时维护一个三角形数量tripleSum的变量,在查询时直接返回即可

    public void triSumAdd(Person person1, Person person2, int plus) {
        for (Person person : personArrayList) {
            if (person.getId() != person1.getId() &&
                    person.getId() != person2.getId()) {
                if (person.isLinked(person1) && person.isLinked(person2)) {
                    triSum += plus;
                }
            }
        }
    }

第二次作业

第二次作业新增加了Tag功能,并且实现了最短路径查找

在查找关系最好的人员queryBestAcquaintance时,采用了维护中间量的方法,为每一个Person维护一个bestId的中间变量,在增加、调整和删除关系时重新查找bestId,在查询时直接返回即可

    public void findBestId() {
        int i = 0;
        bestId = 0;
        for (Map.Entry<Integer, Integer> entry : value.entrySet()) {
            if (i == 0) {
                bestId = entry.getKey();
            } else {
                if (Objects.equals(value.get(bestId), entry.getValue())) {
                    bestId = bestId < entry.getKey() ? bestId : entry.getKey();
                } if (value.get(bestId) < entry.getValue()) {
                    bestId = entry.getKey();
                }
            }
            i++;
        }
    }

在查找最好关系queryCoupleSum时,由于实现了bestId的维护,只需遍历所有有关节点,检查是否互为bestId即可,最终数目的一半就是返回值

    public int queryCoupleSum() {
        int sum = 0;
        for (Person person : personArrayList) {
            if (((MyPerson)person).getSizeOfAcquaintance() != 0) {
                Person bestPerson = persons.get(((MyPerson)person).getBestId());
                if (((MyPerson)bestPerson).getBestId() == person.getId()) {
                    sum++;
                }
            }
        }
        return sum / 2;
    }

在Tag查询边权值之和时queryValueSum时,我才用了bfs广度优先遍历,将所有的便全部遍历一遍,边权值之和的二倍就是最终的sum结果

    public int getValueSum() {
        setFlags();
        int sum = 0;
        for (Person person : personArrayList) {
            if (((MyPerson) person).getFlag()) {
                sum += ((MyPerson) person).dfsSum(persons, new HashMap<>());
            }
        }
        setFlags();
        return 2 * sum;
    }

在查询最短路径queryShortestPath中,我才用广度优先搜索bfs进行查询,查到目标节点后返回最短路径,进而知道最短长度

    public ArrayList<Person> bfs(Person start, Person end) {
        setFlags();
        Queue<Person> queue = new LinkedList<>();
        queue.add(start);
        ((MyPerson)start).setFlag(false);
        while (!queue.isEmpty()) {
            Person current = queue.poll();
            if (current == end) {
                return buildPath(start, current);
            }
            ((MyPerson)current).bfsPart(queue);
        }
        return null; // 没有找到路径
    }

    public ArrayList<Person> buildPath(Person start, Person end) {
        ArrayList<Person> path = new ArrayList<>();
        Person current = end;
        while (current != start) {
            path.add(0, current);
            current = ((MyPerson)current).getFather(); // 父节点
        }
        path.add(0, start); // 起始节点
        return path;
    }

第三次作业

第三次作业并没有什么算法难度,实现了消息发送的方法

在删除冷门表情deleteCodeEmoji和删除notice消息中,采用了java提供的removeIf方法进行删除

    public int deleteColdEmoji(int limit) {
        emojis.values().removeIf(v -> v < limit);
        messages.values().removeIf(message ->
                message instanceof  EmojiMessage &&
                        !containsEmojiId(((EmojiMessage) message).getEmojiId()));
        return emojis.size();
    }

    public void clearNotices() {
        messages.removeIf(message -> (message instanceof NoticeMessage));
    }

作业中出现的性能问题及其修复情况,以及对规格与实现分离的理解

第一次作业当中由于三角形数量的维护并未很好实现,导致强测出现了CTLE

第二次作业查询valueSum时由于采用了bfs算法,导致时间复杂度过高,并且在互测当中被卡了栈溢出错误

第三次作业由于忽视了size==0的情况,强测发生了三个错误,原因是没有及时更新官方规格包,进行特判即可

JML规格与实现分离: JML是对 Java 代码的预期行为进行规格化描述,这些描述是独立于代码实现的。JML 规格可以用于生成测试用例,也可以用于静态分析工具来检查代码的一致性。通过分离规格和实现,JML 有助于确保代码的实现符合预定的行为规格,提高软件的质量和可维护性。

如何利用规格信息来更好的设计实现Junit测试,以及Junit测试检验代码实现与规格的一致性的效果

用 JML 规格信息设计 JUnit 测试:

根据JML规格,确定每个方法或类应有的行为。

设计测试用例来覆盖规格中描述的所有预期行为。

使用 JUnit 框架编写测试代码,使用断言来验证实际输出与规格描述的预期输出是否一致。

运行测试,并根据测试结果调整代码或测试用例。

JUnit 测试检验代码实现与规格一致性的效果: JUnit 测试可以有效地检验代码实现是否与 JML 规格描述的行为一致。通过运行一系列的测试用例,JUnit 测试可以检查代码的每个部分是否都按照规格书执行。如果测试通过,则说明代码实现与规格一致;如果测试失败,则说明代码可能存在缺陷或与规格不符,需要进一步调查和修复。这种方法有助于确保软件的质量,并减少在开发过程中引入的错误。

本单元学习体会

第三单元的主要侧重于JML规格的理解和相应的算法实现,当JML规格变得越来越复杂时,阅读理解就成了一个必不可少的部分,对JML的规格理解以及代码实现有助于我更好的理解方法实现的规范,体会代码编写的规范化历程。

希望下一单元能够学到更多Java和面向对象的知识

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

301

社区成员

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

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