B U A A 第 三 单 元 总 结

安琦22373295 学生 2024-05-17 15:08:31

一、分析测试过程

黑箱与白箱测试

黑箱测试和白箱测试是软件测试中常用的两种测试方法,它们在测试的角度、方法和目的上有所不同。

黑箱测试是一种测试方法,测试人员不需要了解内部代码或系统结构,而是基于软件的需求规格说明和功能特性来设计测试用例,并验证软件的功能是否符合预期。黑盒测试关注的是软件的外部行为,旨在检查软件是否能够正确地处理输入数据并产生正确的输出。黑盒测试可以帮助发现功能性问题、性能问题、安全问题等。

白箱测试则是一种测试方法,测试人员需要了解软件的内部代码、结构和算法等信息,基于这些信息设计测试用例以揭示软件的内部错误和缺陷。白盒测试关注的是软件的内部逻辑、路径覆盖和代码覆盖等方面,旨在验证代码的正确性、健壮性和可维护性。白盒测试可以帮助发现代码逻辑错误、循环和条件覆盖不足等问题。

单元测试

单元测试是针对软件中最小的可测试单位(如函数、方法)进行的测试。单元测试旨在验证各个单元的代码是否达到预期的功能要求,帮助开发人员在编写代码时及时发现和修复错误。

功能测试

功能测试是以软件功能和需求为基础,测试软件是否按照用户的需求规格说明书执行,确保软件的功能符合预期并且达到要求的质量标准。

集成测试

集成测试是将已通过单元测试的模块结合在一起进行测试,验证各个模块之间的交互与通信是否正常,确保系统各部分协同工作正常。

压力测试

压力测试旨在评估系统在极端条件下的稳定性和性能表现,通过模拟大量用户访问或极大负载来测试系统的极限性能,并确定系统的承受能力和性能瓶颈。

回归测试

回归测试是在对软件进行修改后,重新运行既有的测试用例,以确保修改后的代码没有破坏现有功能和引入新的问题。回归测试有助于在开发过程中保持软件质量的稳定性。

数据构造策略

  • 测试时,应针对边界值和特殊值进行重点测试,例如生成personId-2,147,483,6482,147,483,647附近的值的数据,又如,在生成add_red_envelope_message指令时,应考虑tagSize为0的情况
  • 进行大数据量测试以发现项目的性能问题,例如在本次测试中将指令数目设置为10000条
  • 测试时应注重指数据多样性,应覆盖所有方法进行测试,同时在方法内部,应当尽可能覆盖所有分支,即包括所有的normal_behaviorexceptional_behavior
  • 测试时需要注重数据关联性,本次作业中,指在数据生成时需要利用已经生成的personIdtagIdmessageId等数据,保证生成数据之间存在逻辑关联
  • 在保证数据多样性和关联性的基础上,需要注意数据生成的随机性,即利用Random类生成随机数据

架构分析

图的构建

在本单元的架构中,我使用类似邻接链表的架构存储节点及其连接关系,即以下代码:

HashMap<Integer, HashMap<Integer, Integer>> links

在添加节点时,只需新增<节点,空HashMap>的映射关系,再添加/删除边时,只需在节点对应的HashMap新增/删除<对应节点,1>的映射关系,这种设计便于维护图的动态变化,也为深度/广度优先搜索提供了数据结构上的基础。

图的维护

连通性

在维护图的连通性时,本次作业使用了并查集的思路。即在Network类中维护一个名为parent的HashMap用于存储节点的父节点,每次新增边时,调用union方法合并对应的两个节点。为提高性能,我采用了路径压缩和按秩合并的优化思路,即在搜索根节点时,通过循环更新的方法把父节点更新为根节点,在合并节点时,利用记录每个节点秩的HashMap,将秩较低的树合并至秩较高的树上。

 public int findRoot(int id) {
        int temId = id;
        while (temId != parents.get(temId)) {
            int oldParent = parents.get(temId);
            parents.replace(temId, parents.get(oldParent));
            temId = oldParent;
        }
        return temId;
  }
  public void union(int id1, int id2) {
        int root1 = findRoot(id1);
        int root2 = findRoot(id2);
        if (root1 == root2) {
            return;
        }
        if (ranks.get(root1) < ranks.get(root2)) {
            parents.replace(root1, root2);
        } else {
            parents.replace(root2, root1);
            if (ranks.get(root1).equals(ranks.get(root2))) {
                int oldRank = ranks.get(root1);
                ranks.replace(root1, oldRank, oldRank + 1);
            }
        }
    }

需要注意的是,并查集不能解决删边的问题,而利用深度优先搜索重构并查集的时间复杂度为O(N),因此我采用了一种折中的方法,即删除边时,将delFlag置为true,当调用isCirclequeryBlockSum等需要检验节点连通性的方法时,若检测到delFlag为true,再重建并查集,并将delFlag置为false。这样的策略可以避免反复删除边但不检查联通性而导致的性能损失。

具体操作上,重建并查集时需要利用深度优先搜索算法,即从第一个节点开始,将从该节点开始可以搜索到的且未被访问的所有节点的父节点设置为该节点,然后对下一个未被访问的节点进行相同操作,直至所有节点都被访问,此时并查集重建完成。

   public void rebuildGraph() {
        HashMap<Integer, Integer> visited = new HashMap<>();
        for (int i : links.keySet()) {
            updateParent(i, visited, i);
        }
        dirtyFLag = false;
    }

    public void updateParent(int id, HashMap<Integer, Integer> visited, int root) {
        if (!visited.containsKey(id)) {
            parents.replace(id, root);
            visited.put(id, 0);
            for (int i : links.get(id).keySet()) {
                updateParent(i, visited, root);
            }
        }
    }

triangleCount

每次在{节点1,节点2}之间新增/删除边时,都需要从link中遍历节点1的所有邻接节点,若其与节点2相连,则将triangleCount加一/减一,这一操作的最坏时间复杂度为O(N),能满足性能要求(以下为新增边时的代码)。

 for (int i : links.get(id1).keySet()) {
      if (getPerson(i).isLinked(p2)) {
          triangleCount++;
      }
 }

bestAcquaintance

这里采用了比较简单的思路,当新增{节点1,节点2}之间的边时,均将对方与自己原有的bestAcquaintance比较并判断是否更新,当需要修改两节点关系的value或删除边时,则将changeFlag置为true。当需要获取bestAcquaintance时,若changeFlag为true则遍历该节点的acquaintance并计算出bestAcquaintance

相较于这种思路,使用TreeMap的方法在修改时时间复杂度为O(logN),在查询时时间复杂度为O(1)。针对大多数测试样例,两种实现时间差距不大,但本次作业的实现更加适用于频繁修改但查询次数少的情况,而对于删除/修改value+查询的组合,受限于指令数目,性能并不会出现问题。

public void addNewRelation(Person person, int value1) {
        int addedId = person.getId();
        acquaintance.put(addedId, person);
        value.put(addedId, value1);
        if (!changeFlag) {
            if (value1 > maxValue || value1 == maxValue && person.getId() < bestId) {
                bestId = person.getId();
                maxValue = value1;
            }
        }
}
public int getBestAcquaintance() {
        if (changeFlag) {
            int temMaxValue = -1;
            for (int i : value.keySet()) {
                if (value.get(i) > temMaxValue || value.get(i) == temMaxValue && i < bestId) {
                    bestId = i;
                    temMaxValue = value.get(i);
                }
            }
            changeFlag = false;
            maxValue = value.get(bestId);
            //重新确定最小
        }
        return bestId;
    }

valueSum

每个tagvalueSum均应当从两个方面进行维护:

  • 当向tag内部添加节点时,需要遍历内部的所有节点,若与添加的节点邻接,则修改valueSum
  • 当修改两个节点间的关系(新增,删除或修改value)时,则需要遍历所有tag(即先遍历person,再遍历每个person内的tag),若tag内存在这两个节点,则修改valueSum

这种思路将时间复杂度分摊到了每次新增节点与修改节点间关系的操作中,每次操作的时间复杂度为O(N),避免了使用二重循环导致的性能问题(以下为添加节点时的代码)

 public void addPerson(Person person) {
        for (int i : persons.keySet()) {
            if (person.isLinked(persons.get(i))) {
                valueSum += 2 * person.queryValue(persons.get(i));
            }
        }
        persons.put(person.getId(), person);
        ageSum += person.getAge();
        size++;
}

均值和方差

在上一段代码中可以注意到,tag内部维护了ageSum变量,可以随时利用ageSum/size计算均值。而在计算方差时,由于取整操作,必须代入原公式进行计算,其时间复杂度为O(N)。

shortestPath

由于节点间距离均为1,只需采用广度优先搜索即可。具体思路是从起点开始搜索,每访问到一个节点时,将其未被访问的邻接节点的距离设置为当前节点的距离加一,当访问到终点时搜索结束,其时间复杂度为O(N)。

 public void bfsSearch(int searched, int target
            , HashMap<Integer, Integer> visited, Queue<Integer> queue) {
        visited.put(searched, 0);
        boolean flag = false;
        if (searched == target) {
            return;
        }
        queue.add(searched);
        while (!queue.isEmpty() && !flag) {
            int needToSearch = queue.poll();
            int basicLength = visited.get(needToSearch);
            for (int i : links.get(needToSearch).keySet()) {
                if (!visited.containsKey(i)) {
                    visited.put(i, basicLength + 1);
                    if (i == target) {
                        flag = true;
                        break;
                    }
                    queue.add(i);
                }
            }
        }
    }

性能问题及修复

本次作业全程未出现性能问题,主要原因在于,在设计架构时我就对每个方法的时间复杂度进行了分析,不允许任何方法出现大于等于O(N^2)的时间复杂度。同时,在测试时,我构造了高强度数据对性能进行极限测试。

规格与实现分离

这种策略是指将规格与具体实现分离,以实现更好的模块化、可维护性和可扩展性。其中规格描述了软件的功能需求、性能需求、接口规范等信息,而在具体实现时,只需要保证实现满足规格的要求,不需要过多考虑需求的细节。

当软件的需求发生改变时,只需要修改规格,而不需要直接修改代码。这降低了修改代码的风险和成本。规格与实现相分离也使得代码更加模块化,便于后续的维护和扩展。

规格为测试人员提供了明确的测试依据,他们可以基于规格编写测试用例,验证实现的正确性。由于规格是独立于实现的,因此测试可以更早地开始,并与开发并行进行,从而提高开发效率。

JUnit测试

在构造测试样例时,需要利用规格中的前置条件覆盖规格中的每种情况,即正常行为和异常行为均需要进行测试。在检验正确性时,需要对以下内容进行测试:

  • 对于存在返回值的方法,需要对返回值进行测试
  • 对于规格中说明不进行修改的成员,需要验证其未被修改,特别的,对于pure方法,需要保证对象中的所有成员均未被修改
  • 对于规格中说明允许被修改的成员,在测试其是否满足方法内要求的同时,还需要测试其是否满足不变式约束和状态变化约束

通过使用assert系列方法,我们可以检测JUnit测试数据输出与规格输出是否一致。

学习体会

相较于前两个单元,本单元简单了许多,在JML规格方面也并未设置过大的难度。在实际编写代码时只需要对照规格逐个方法逐个分支实现即可,在测试时则需要对照规格构造测试数据,也没有特别大的难度。相反的,我觉得本单元更多的考查了算法相关的内容,尤其侧重图论方面的内容,这反而给我代码的编写带来了较大的阻碍。

通过三次作业,我理解了JML的重要性,它避免了自然语言的不确定性,能提升软件开发的效率与准确性。

...全文
42 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复
内容概要:本文围绕基于NSGA-2算法的电-热-气综合需求响应(IDR)在综合能源系统中的多目标优化问题展开研究,提出了一种融合智能优化算法与能源系统协同调度的解决方案。研究系统性地构建了包含电力、热力与天然气多能耦合的综合能源系统模型,设计了以能效提升、运行成本降低和环境效益优化为目标的多目标优化框架,并采用NSGA-2算法求解该复杂非线性优化问题,获得Pareto最优解集。文中详细阐述了IDR机制建模方法、多目标优化问题的数学建模过程以及NSGA-2算法的关键实现步骤,包括编码方式、交叉变异策略与非支配排序机制,并通过Matlab代码实现了完整的仿真验证流程,展示了该方法在平衡多个冲突目标方面的优越性能。; 适合人群:具备电力系统、能源系统建模或优化算法基础的研究生、科研人员及工程技术人员,特别适用于从事综合能源系统规划、需求响应策略设计、多目标进化算法应用等相关领域的研究人员; 使用场景及目标:①应用于综合能源系统的运行优化与规划决策,支持多能互补场景下的协同调度分析;②为科研工作者提供NSGA-2算法在能源系统中实际应用的完整Matlab代码实现范例,助力学术论文复现、算法改进与模型拓展; 阅读建议:建议读者合文中提供的Matlab代码,按照系统建模、目标函数设计、约束处理到算法实现的研究脉络逐步学习,重点关注Pareto前沿的生成过程、多目标权衡分析及算法参数敏感性,以深入掌握多目标进化算法在复杂能源系统优化中的工程化应用方法。

301

社区成员

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

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