oo第三单元博客作业

王书勤-22373507 学生 2024-05-16 12:01:56
 

测试过程

黑盒测试&白盒测试

黑盒测试是不需要了解软件系统的内部结构或实现细节,仅仅基于系统的功能规格和需求来设计测试用例和测试场景。白盒测试是基于对代码的理解验证系统的内部逻辑是否正确,是否覆盖了所有的路径和分支。

本单元中最常用和最直接的测试方式仍然是黑盒测试。在初步编写完代码后,黑盒测试可以用大量数据测试代码是否能正常运作,是否在部分情况会出现bug。以往的我盲目相信黑盒测试,以至于认为通过了评测机的考验就大概率是没有bug的。这样忽略了两点,其一,评测机也是人造的,如果搭建评测机的人没有意识到某个可能出现bug的点,或者根本理解错误,那么该评测机也是错的。其二,评测机大多是通过随机生成数据,其中会有很多指令是触发异常的无效指令,即使在大量数据的倾倒下真正有用的信息也不多,针对本次复杂的关系网络,很难保证已经测试到所有的情况。

我对白盒测试比较直观的理解是人脑构造数据尽可能覆盖所有情况。构造数据已经很麻烦了,还要绞尽脑汁覆盖所有情况,这显然是工作量很大的事情。对于一些逻辑简单的代码,白盒测试似乎就没有很必要。而对于一些过于复杂的情况,比如计算blockSum,该怎样才算是“覆盖了所有情况”呢?也难以界定。所以,白盒测试(至少在这样小规格项目中)适用情况并不多。但有一种情况尤其适合白盒测试,即涉及到很多隐性的if...else判断的方法。比如本单元中coupleSum的计算,其值只可能在增加或者修改关系的时候变化,这时候就尤其适合进行白盒测试,检查每一种情况是否检测到了变化并正确记录,以及是否有效的修改。

单元测试

单元测试是最小级别的测试,通常由开发者编写和执行,用于验证代码中的最小可测试单元(如函数、方法、类等)的行为是否符合预期。

功能测试

功能测试验证软件系统是否按照需求规格说明书的规定正常实现,关注于软件系统的功能是否完整、正确,以及是否满足用户需求。

集成测试

集成测试在单元测试之后进行,旨在验证软件系统中不同模块之间的接口和交互是否正常,关注于模块之间的集成和交互,确保它们能够协同工作并产生正确的结果。这个主要是可以发现模块之间的接口问题、数据传递问题以及功能冲突。在本单元中JML已经规定好一切,故不涉及集成测试,然而该方法在其他面向对象编程是非常重要的测试方法。

回归测试

回归测试是在软件开发生命周期的后期阶段进行的,用于验证之前已经通过测试的代码在修改后是否仍然保持原有的功能和性能。当开发者修复了软件中的一个缺陷或增加了新的功能后,需要执行回归测试来确保这些修改没有引入新的问题或导致旧的问题重新出现。回归测试在协作项目中应该有很大的帮助,对于我们而言即是测试本次作业的修改是否会导致之前作业某项功能错误或无法正常实现。

数据构造

  • 基于需求构造,模拟正常情况和异常情况

  • 根据指定的规则或范围生成大量随机数据

  • 针对输入数据的边界值进行测试,如负数、范围边缘

  • 构建不同情况的数据,如稀疏图和稠密图

架构框架

总体来说按照JML固定的架构完成,对于异常情况设置了一个Counter类完成计数,对于Network中图模型设置了一个Dsu类使用存储图结构并使用并查集算法管理连通性问题。

性能优化

数据结构

本次作业尝试了多种容器管理,以MyPerson类为例

//通常使用PersonId查找Person或判断是否相识,故使用HashMap    
private HashMap<Integer, Person> acquaintance;
//需要按照Value的大小对Person进行排序,主要服务于判断bestAcquaintance
//同时需要不断增加、修改和删除元素,PriorityQueue难以满足需求,故使用TreeSet
private TreeSet<Person> acquaintanceByValue;
//message需要按照顺序存储,并且涉及取出最新的五条消息、将新增消息插入至队首的操作,故使用LinkedList
private LinkedList<Message> messages;

值得一提的是TreeSet需要重写CompareTo方法,如果相减可能会超int范围,当然也可以这样写,不是最简洁但比较稳妥

this.acquaintanceByValue = new TreeSet<>(((o1, o2) -> {
            int id1 = o1.getId();
            int id2 = o2.getId();
            int vc = Integer.compare(values.get(id1), values.get(id2));
            if (vc != 0) {
                return -vc;
            }
            else {
                return Integer.compare(o1.getId(), o2.getId());
            }
        }));

图的管理

    private HashMap<Integer, Integer> parent;  //节点的父节点
    private HashMap<Integer, Integer> rank;  //方便按秩合并
    private HashMap<Integer, HashSet<Integer>> graph;  //图的原始结构
    private HashMap<Integer, HashMap<Integer, Integer>> counted;  //计算过的两点之间的ShortestPath
    private boolean countedDirty;  //若Dirty则需要将counted清零重新计算
    private int blockCnt;
    private int tripleCnt;

Dsu类只存了连通性而没有存值,所以在Network中需要更改的只有

  • addPersonadd结点

  • addRelationunion

  • modifyRelation中若为断关系则breakup

blockCnt连通块个数

  • 加人的时候++

  • 加关系时若导致两块连通--

  • 删关系时若导致分裂成两块++

tripleCnt三元闭包个数

  • 加关系的时候先加再查

  • 删关系的时候先查再减

CoupleSum

    private int coupleSum = 0;
    private boolean coupleDirty = true;  //由checkBefore()和checkAfter()比对判断是否需要设置脏位
    private HashSet<Person> noMatch = new HashSet<>();

ValueSum

//间接管理
private void modifyValueSum(Person p1, Person p2, int oldValue, int newValue) {
        for (Person person : ((MyPerson) p1).getAcquaintance()) {
            if (person.getId() != p2.getId() && person.isLinked(p2)) {
                for (Tag tag : ((MyPerson) person).getTags()) {
                    if (tag.hasPerson(p1) && tag.hasPerson(p2)) {
                        ((MyTag) tag).modifyValueSum(oldValue, newValue); } } } }
        if (delete) {
            submodify(p1, p2);
            submodify(p2, p1); }
        delete = false;
    }
​
//直接管理
private void submodify(Person p1, Person p2) {
        for (Tag tag : ((MyPerson) p1).getTags()) {
            if (tag.hasPerson(p2)) {
                for (Person p : ((MyTag) tag).getPersons()) {
                    if (p.getId() != p2.getId() && p.isLinked(p2)) {
                        ((MyTag) tag).modifyValueSum(p2.queryValue(p), 0); } } } } }

EmojiMessages

考虑到信息数量可以非常多,所以也加上了一点点优化,可以省去对message的遍历

private HashMap<Integer, Message> messages = new HashMap<>();
private HashMap<Integer, Integer> emojis = new HashMap<>();
private HashMap<Integer, HashSet<Integer>> emojiMessages = new HashMap<>();
​
public int deleteColdEmoji(int limit) {
        emojis.entrySet().removeIf(entry -> entry.getValue() < limit);
        Iterator<Integer> iterator = emojiMessages.keySet().iterator();
        while (iterator.hasNext()) {
            int eid = iterator.next();
            if (!emojis.containsKey(eid)) {
                for (Integer mid : emojiMessages.get(eid)) { messages.remove(mid); }
                iterator.remove(); } }
        return emojis.size();
    }

JUnit

在编写测试时,我们可以将JML规格视为一种模板或蓝图,然后根据这些规格来编写代码和测试。这种直白的“翻译”过程确保了代码和测试与规格保持一致,从而提高了代码的质量和可维护性。

总体而言测试可以分为这样几步:

  • 实例化对象构造数据

  • 调用方法得到结果

  • (异常)判断异常是否能被正常捕获

  • (正常)逐行判断ensures是否满足

心得体会

JML描述的前置条件、后置条件和副作用,可以使得代码更容易被程序员理解和维护,也便于我们编写测试来验证代码的正确性。理论来讲,本单元最大的收获应该是学习如何编写更清晰、更可靠、更易于维护的代码。然而实际上,由于测评形式原因,感觉本单元更多的精力还是用在优化算法上了,虽然也是学习指标中的一环,但似乎有点本末倒置??总归来讲有学长学姐博客助力实现压力已经小很多了,课程组也对JML进行合理的优化减少不必要的困扰,还是很好的。

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

301

社区成员

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

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