2024-BUAA-OO第三单元作业心得

22060106-朱李浩 2024-05-18 16:54:40

2024-BUAA-OO第三单元作业心得

可算是熬完了OO的第三单元,这一单元给我的感觉甚至比第一单元还要恐怖,在这一单元,第一次上机没有做出来,以及另一次上机做出来了也不确定自己是不是对的,这种感觉从来没有这么强烈过。其实个人主观上来说,这一单元的复杂程度甚至高于第二单元,略低于第一单元。虽然得分比第二单元好些,但是仍旧折磨。

这一单元主要为规格设计方面,主要要求我们能够根据程序的需求设计相应的方法规格、类规格,根据规格书写代码,最后基于规格开展测试(其实由于时间原因,似乎使用的并不多,但是也付出了相应的代价)。

一、测试过程分析

这个单元中,其实主要方法的jml都已经在官方包中提供了,在编写程序的过程中只要理解jml的含义,便能够在一定程度上支持程序的正常功能撰写,当然,也有些情况下单独从jml似乎无法直接推知方法的作用。但是jml仍提供了一种有效的测试约束,使得在具体对方法的测试中能够自动化,且有效地进行。

1、黑箱测试、白箱测试

黑箱测试和白箱测试是软件测试的两种常用方法。

黑盒测试方法的独特之处在于执行该测试的测试人员不了解他们正在测试的软件的内部结构和源代码。而且他们不需要任何东西,也不需要具备对编程语言的深入了解或出色的编码技能来执行测试。这主要是因为此测试方法的目标不是深入研究代码,遍历软件内部,而是与用户界面进行交互,测试其功能,并确保系统的每个输入和输出均符合标准。指定要求。因此,黑盒测试也可以称为功能测试或基于规范的测试。

该方法几乎适用于软件测试的每个级别:单元,集成,系统和验收。在单元测试中,黑盒方法用于根据客户端给出的规范测试接口。

与侧重于功能的黑盒测试相反,白盒测试方法的目标是对软件的内部结构及其背后的逻辑进行分析。因此,白盒测试有时称为结构测试或逻辑驱动测试。这种方法非常耗时,并且要求测试人员具有强大的编码技能,对他们正在测试的软件的全面了解,并且可以访问所有源代码和体系结构文档,否则,测试人员将无法设计适当的测试用例。因此,白盒测试通常是由专业开发人员执行的,他们使用他们的专业知识来获得结构的内部观点,弄清楚源代码中到底发生了什么,并修复了无法正常工作的问题。除了深入的知识外,该方法还需要用于源代码分析和调试的专用工具。

测试人员彻底研究给定软件的代码和其他内部方面,确定所有有效和无效输入,然后根据预期结果验证输出。他们检查语句和条件,代码路径和数据流,以确保没有隐藏的错误或容易出现缺陷的元素。

白盒测试可以应用于单元测试级别,但如今主要用于集成测试和回归测试。该方法使测试人员可以检查单元内的路径是否存在代码缺陷和其他可能导致软件无法按预期工作的问题。这是在与以前测试的代码进行任何集成之前完成的,从而降低了在以后的开发中出错的风险。

2、单元测试、功能测试、集成测试、压力测试、回归测试

单元测试:单元测试是针对代码中最小的可测试单元(例如函数、方法)进行的测试,目的是验证这些单元的行为是否符合预期。例如我们在做第三次作业时对deleteColdEmoji方法进行验证,对多种ensure进行检查,确定其影响以及结果的正确

功能测试:功能测试是通过模拟用户使用软件来验证整个应用程序是否按照用户需求正确工作的测试。功能测试通常涉及多个组件和系统,它可以手动或自动执行。也就是说,功能测试类似于上文中提到的黑箱测试,我们需要从用户的视角来看,给出任意合法的输入和预期输出,与程序输出进行比较,从而判断应用程序的正确性。

集成测试:集成测试是将多个单元或组件组合起来以检查它们之间的交互是否如预期般工作的测试。它可以确保各组件之间的接口一致性和兼容性。集成测试主要目的是测试多个模块之间的数据通信,因为尽管每个模块都通过了单元测试,但是由于不同模块之间的接口,异常处理方式依然可能存在问题,所以需要将多个模块组合起来测试它们协同工作的能力。

压力测试:压力测试是通过模拟真实环境下大量用户访问应用程序来测试应用程序性能和稳定性的测试。该测试可以帮助识别应用程序在高负载情况下的响应时间和资源使用情况,并确定其能够承受多大的负载。简单来说,我们作业的强测环节就是一种压力测试,在输入大量数据的情况下来判断程序的正确性,同时用CPU时间来衡量程序的性能,有效测试了我们的程序在高负载情况下的运行能力。

回归测试:回归测试是在对软件进行更改后重新运行先前已经运行过的测试,以确保没有已有功能被意外地破坏。它可以帮助开发人员快速发现并修复由新更改引入的错误,以确保软件的质量和稳定性。也就是说,我们在修改bug或者是修改算法来提高程序性能时,有没有写出新的bug(bushi),通常这种情况是很常见的,所以我们必须在修改一次程序之后就要用之前测试过的数据来对修改后的程序加以测试,来验证新程序是否具有一定的正确性。使用Gitlab中的CI/CD管道就可以很好地对我们的代码开展回归测试。

3、数据构造策略

本单元作业的测试采用的为Junit的测试工具,这个工具具体是分为了两部分,一部分为@Parameterized.Parameters,这部分主要为数据的生成,另一部分自然便为test部分,主要测试相应的方法。在数据生成部分,可以以数组的方式生成多组数据并加载,最后每组数据可以跑一个test测试,最后返回相应的测试情况。

这三次作业中,我并没有采用直接手动构造数据的方式,一个原因是因为可能个人能力较差,没能够有足够的时间去分析所有边界情况进行测试。另一个原因是担心个人能力不足,并不能进行全面的覆盖。因此,数据构造策略采用的多组数据,模拟数据构建操作的自动生成模式。具体采用随机数生成数据。以第一次作业为例,生成了10组测试数据(可调),然后通过循环进行addperson,addRelation,modifyRelation的操作,随机操作两人之间的关系,从而生成不一样的数据。具体的循环次数,人数,relation值的范围,均通过变量的形式进行加载,从而实现差异化,以及控制不同的稀疏程度等。

同时,设计了两个network类,一个作为原始数据,一个作为操作数据。分别命名为beforeMyNetWork,afterMyNetwork,两者在数据构造阶段进行完全相同的操作。这样做的好处是,可以尽可能保留原始数据,方便对pure方法进行检验,也方便对一些不好get到的属性进行先一步的保存与计算。

以下为一个简单的例子,展示两者同步操作的数据构造阶段。

    for (int j = 0; j < acquaintanceNum; j++) {
        int pid1 = 0;
        int pid2 = 0;
        while (pid1 == pid2) {
            pid1 = random.nextInt(personNum);
            pid2 = random1.nextInt(personNum);
        }

        int value = random.nextInt(21) - 10;
        if (!tempBeforeMyNetWork.getPerson(pid1)
                .isLinked(tempBeforeMyNetWork.getPerson(pid2))) {
            try {
                tempBeforeMyNetWork.addRelation(pid1, pid2, value);
                tempAfterMyNetWork.addRelation(pid1, pid2, value);
            } catch (PersonIdNotFoundException | EqualRelationException e) {
                throw new RuntimeException(e);
            }
        } else {
            try {
                tempBeforeMyNetWork.modifyRelation(pid1, pid2, value);
                    tempAfterMyNetWork.modifyRelation(pid1, pid2, value);
            } catch (PersonIdNotFoundException |
                        EqualPersonIdException | RelationNotFoundException e) {
                throw new RuntimeException(e);
            }
        }
    }

二、架构设计梳理

因为个人基础比较差,图算法确实对我来说比较难,同时对于时间复杂度的分析与改善也不甚熟悉,因此这里能说的可能比较少。

主要的设计想其实是在新增比如人,关系的时候,直接通过在新增时变维护一些变量。因为在期望的使用情况中,应该主要为查询等操作为主,更改关系的操作为次要操作。这样构造可以使得查询操作的时间大幅缩短,只是在操作关系的时候会多花费一些时间。

第一次架构:

1.png

第二次架构:

2.png

可以看到主题上基本仍采用了官方包中间的接口,但是新增了一些用以维护与计算的方法与类,同时也方便每种方法等满足checkStyle的规范要求。

三、性能问题及修复情况

因为真的不熟悉图算法,所以基本上都是照葫芦画瓢艰难操作的,也不清楚各个的优势以及劣势。在第一次的作业中,主要使用的为广度优先搜索,后面换成了深度优先搜索,但是其实效果还是不太理想。同时在实际操作中,仍然大规模使用了JML中使用的循环遍历的方法,导致了有多层循环,效率其实不太好。特别是qts等指令的效率还是比较低下的。同时,复杂的图算法等的检索机制,也导致了计算数据结果的问题,比如负数这块还处分了一些计算不正确的bug,导致第二次和第三次作业的强测得分不甚理想,以及在互测中被超时以及边缘数据hack了不少次。

对于规格与实现分离的思想,个人感觉其实做的不太好。因为在实际的代码实现中,还是大规模采用了规格中间所描述的实现方式。这些实现方式,虽然可能比较简单,且check起来行之有效,但是对于算法的性能是不利的,多层的循环遍历会导致算法的效率十分低下。

规格中间描述方法,更多的应该是推荐的junit test的方法,因为相比于实现中,规格测试的时候能够获得更多且更有效的数据,因此这种转换成代码更加简单的描述语句,可以帮助你在编写具体测试流程的时候也减少很多bug。同时,规格的分离化描述,也有助于对每一个需要ensure或者其他的条件进行单独测试。相比于算法的高度集成化与专一化,jml的分离描述确实能够做到更加全面的覆盖。

四、Junit测试

本单元的每一次作业,都会要求对MyNetwork中的一个方法进行junit测试,个人感觉难易程度分别为,简单,难,中等。

在junit测试中,其实个人感觉分为了两个部分。第一个部分为对副作用结果的检测,这一块主要面向的是pure或者assignable的范围,保证副作用范围有限且正确。具体的操作为,首先,你需要通过某种方式保存他之前的属性结果,比如persons等。这块,有两种方式,一种可以通过已经确认不会出错的get方法,把你想保存的结果get出来,然后深拷贝存一份。另一种方式是直接在构造数据的时候就把你需要用的数据算好,存着。具体实现中我使用的为第二种方法,但是其实第一种办法的通用性更高,可以有效地将测试数据用于更多的测试中。第二部,则是面对ensure依次编写测试语句,这其中的建议是不要优化任何算法和流程,他写什么操作你就写什么操作,直接一对一翻译就好。因为这个过程中,在能保证jml正确性的情况下,直接翻译能够保证test的正确性,不会出现缺漏。

但是在以上的过程中,我一直在考虑一个问题,这也是我在第二次作业的test编写时候遇到的,当你维护了一个自己的变量,这个变量并不存在于官方提供的接口要求中的时候,是否意味着,这个变量我无法使用第一种方式去get到他的初始值并进行保存了?还真是的。因此只能够采用第二种了,其实这时候限制真的是很大的了,你相当于要在构造数据时进行一整套的计算流程,算出来这个值最后拿在手上,不然,这个值外面是根本不可见的。因为在第二次作业中维护了比较多的自己的属性,因此第二次作业的test我是觉得最难的,1,3次反而相对简单。

第三次作业其实换了一个方向,主要针对的是ensure的要求。一大串ensure,第一次看到的时候确实令人震撼,容易让人觉得无从下手。其实正常写起来了以后就不那么觉得了,一个个ensure依次写过去就可以了。详情如下。

    /*@ ensures emojiIdList.length ==
      @          (\num_of int i; 0 <= i && i < \old(emojiIdList.length); \old(emojiHeatList[i] >= limit));*/
    int count = 0;
    for (int i = 0; i < oldIdList.length; ++i) {
        if (oldHeatList[i] >= limit) {
            ++count;
        }
    }
    assertEquals(count, newIdList.length);

    /*@ ensures emojiIdList.length == emojiHeatList.length;*/
    assertEquals(newIdList.length, newHeatList.length);

    /*@ ensures (\forall int i; 0 <= i && i < \old(messages.length);
      @          (\old(messages[i]) instanceof EmojiMessage &&
      @           containsEmojiId(\old(((EmojiMessage)messages[i]).getEmojiId()))  ==> \not_assigned(\old(messages[i])) &&
      @           (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i])))));*/
    for(int i = 0; i < oldMessages.length; ++i) {
        if (oldMessages[i] instanceof EmojiMessage
                && afterMyNetwork.containsEmojiId(((EmojiMessage)oldMessages[i]).getEmojiId())) {
            boolean flag = false;
            for(int j = 0; j < newMessages.length; ++j) {
                if (oldMessages[i].getId() == newMessages[j].getId()) {
                    flag = true;
                    if (!testMessageEqual(oldMessages[i], newMessages[j])) {
                        flag = false;
                        assertTrue(flag);
                        break;
                    }
                    break;
                }
            }
            assertTrue(flag);
        }
    }

个人的认知中,junit测试与规格的一致性,便是通过这种“按部就班”的操作来实现的,不多一个,也不漏掉一个。

五、学习体会

回顾本单元的学习,我们可以看到本单元主要训练我们根据规格实现代码并开展测试的能力。与前面两个单元注重算法的高效性和创新型不同,这一单元重点关注的是算法的严谨性与鲁棒性。

从JML规格书写的角度来看,我们需要保证自己足够严谨,能够正确给出方法的前置条件,副作用和后置条件,保证条件正确无遗漏、无异议。在必要的时候要构造中间数据,采用共性提取机制组合机制等去优化规格,提高规格的可读性。

junit test的模块也为测试带来了一定的便利,它可以以分开的步骤进行测试数据的构造以及方法测试的进行,是一种不错的测试手段,构造数据可以使用多组的构造形式自动化构造,强行保证测试的覆盖,当然,也可以手动“搓”指令进行构造。但是手工编写指令对编写人对边界情况的识别能力要求较高,同时在面对需要强数据测试的时候要求也更高了。

在本单元的学习完成以后,其实个人仍存在一定的疑问,对于jml,以及评测的方面。首先,在评测中,第三次junit测试的评测中,出现了一个我编写了某条ensure,但是缺无法通过评测,把这条ensuree的检查去除以后反而能够通过评测的情况,具体也不知道是我的错误还是提供的代码出错的,因此反而希望在评测结束以后,能够适当地公布评测用的代码情况,让学生能够直观地看到评测错误的原因,同时也能够检验一下是否自己有哪一部分是蒙混过关的。毕竟,有限的评测点其实难以覆盖junit测试中的所有错误情况。比如pure的要求,这个要求会囊括所有属性,在属性数量较多的情况下,是无法覆盖完全的。

其次是对于jml方面,其实个人感觉,对于现在的我(我是菜狗)来说,完全地进行一个方法甚至一个类地jml编写,确实极为有难度。这也大概是所说的“编写jml比写程序要困难的多”的原因了。jml的实际编写需要考虑所有的影响与含义,其实最后已经是对于一个“可行的”检测方式的约束了。但是这样一直给我了一种疑问,怎么确定jml的编写就没有错误了呢?这块一直对我有很大的困扰,难道是靠经验吗,而且jml,似乎也没有一个很完备的测试方式。个人感觉jml的各层都能够出现一定的错误,从jml的编写,到转义成实际代码,都有一定出错的可能,这在我的认知里,开环是没有办法形成负反馈的。

最后,jml感觉,说不定可以有自动生成的方法?感觉这追踪规范化的语言,只要能够进行变量与操作的对应,就可以实现自动生成的检测。也就当个人的一种猜测了。

这一单元中间,因为个人算法能力不足,导致了实现出现了比较多的问题,比如超时,结果不正确等,还是比较困难的。但是jml相对来说了解了具体程序架构与内容以后还是较为容易实现的。实际实现中,没能做好jml与程序实现分离是比较大的问题,没能从规格中“超脱”出去,拥有自己的实现,在利用jml进行验证,比较遗憾。但是jml也确实提供了一种行之有效的测试描述与撰写思路。

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

301

社区成员

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

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