OO课程第三单元总结

概述

本单元主要训练我们基于规格进行程序设计的能力,重点在于根据程序需求设计相应的方法规格和类规格,编写代码并基于规格进行测试。尽管课程主要强调理解和使用JML规格来书写代码,但在三次作业中我们发现,正确实现JML要求的功能只是基础。在此基础上,选择合适的数据结构和算法以提高程序性能也是关键。

一、测试过程分析

作业要求基本通过JML规格提供,因此正确理解JML规格并据此编写测试数据对保证程序正确性至关重要。以下是对一些测试方法的理解及应用。

1. 黑箱测试与白箱测试

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

黑箱测试在不考虑程序内部结构和实现细节的情况下,通过输入数据和预期输出测试软件系统的功能、性能和安全等方面。测试人员只关注软件接口的输入输出行为,不需了解程序的具体实现过程。白箱测试则是在了解程序内部结构和实现细节的基础上,通过各种测试手段验证软件系统的正确性、稳定性、安全性和性能等方面。测试人员需了解代码实现、算法、数据结构等技术细节,利用测试脚本、调试器等工具进行测试。

通俗地说,黑箱测试就像用户测试程序,提供需求和测试数据;白箱测试则要求我们从程序员视角出发,覆盖所有可能的输入数据,并重点测试潜在问题,利用各种工具提高测试强度和覆盖率。

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

单元测试:针对代码中最小的可测试单元(如函数、方法)进行测试,验证这些单元的行为是否符合预期。例如,在第二次作业中对modifyRelation方法进行验证,考虑执行后可能的情况并编写相应测试数据。

public void modifyRelation(int id1, int id2, int value) throws PersonIdNotFoundException,
            EqualPersonIdException, RelationNotFoundException {
        if (contains(id1) && contains(id2) && id1 != id2
                && getPerson(id1).isLinked(getPerson(id2))) {
            MyPerson person1 = (MyPerson) getPerson(id1);
            MyPerson person2 = (MyPerson) getPerson(id2);
            person1.modifyAcq(person2, value);
            person2.modifyAcq(person1, value);
        } else {
            if (!contains(id1)) {
                throw new MyPersonIdNotFoundException(id1);
            } else if (!contains(id2)) {
                throw new MyPersonIdNotFoundException(id2);
            } else if (id1 == id2) {
                throw new MyEqualPersonIdException(id1);
            } else {
                throw new MyRelationNotFoundException(id1, id2);
            }
        }
    }

功能测试:模拟用户使用软件来验证整个应用程序是否按照需求工作。类似黑箱测试,从用户视角提供输入和预期输出,与程序输出比较,判断应用程序的正确性。

集成测试:将多个单元或组件组合测试,检查它们之间的交互是否如预期。确保组件之间接口的一致性和兼容性,测试多个模块协同工作。

压力测试:模拟大量用户访问应用程序,测试其性能和稳定性。帮助识别应用程序在高负载下的响应时间和资源使用情况,确定其承受负载能力。作业的强测环节就是一种压力测试,通过大量数据输入测试程序的正确性和性能。

回归测试:对软件进行更改后重新运行先前的测试,确保已有功能未被破坏。帮助开发人员快速发现并修复新更改引入的错误,确保软件的质量和稳定性。修改程序后需用之前的数据测试新程序的正确性,Gitlab中的CI/CD管道可以很好地开展回归测试。

3. 测试工具的使用

本单元建议使用Junit方法测试,但由于笔者的IDEA无法安装插件,故采用dalao的数据生成器和测试脚本进行随机测试。思路是利用数据生成器生成大量测试数据,通过对拍脚本对多个jar包的输出结果进行对拍,找出不同输出并寻找bug位置。

数据生成器和测试脚本如下:

//数据生成器
int main() {
    int num = 100;
    while (num-- > 1) {
        printf("%d\\\\n", num);
        num_person = num_message = num_group = 0;
        char filename[100];
        memset(filename, 0, 100);
        memset(group_len, 0, 500 * sizeof(int));
        memset(groupPerson, 0, 500 * 1111 * sizeof(int));
        memset(hash, 0, 5000 * 5000 * sizeof(int));
        char nums[100];
        strcat(filename, "hw10datas\\\\\\\\fileIn");
        itoa(num, nums, 10);
        strcat(filename, nums);
        strcat(filename, ".txt");
        FILE *out = fopen(filename, "w");
        for (int i = 0; i < 10; i++) {
            AddPerson(out);
        }
        for (int i = 0; i < 100; i++) {
            generator(out);
        }
    }
    return 0;
}
//测试脚本
$y = 0
for ($x = 0; $x -lt 100; $x=$x+1)
{
    cat ".\\\\hw10datas\\\\fileIn$x.txt" | java -jar hw_10.jar > ".\\\\hw10_out\\\\stdout$x.txt"
    cat ".\\\\hw10datas\\\\fileIn$x.txt" | java -jar xxx.jar > ".\\\\hw10_out_xxx\\\\stdout$x.txt"
    if($(diff  (cat ".\\\\hw10_out\\\\stdout$x.txt") (cat ".\\\\hw10_out_xxx\\\\stdout$x.txt"))) {
        echo "------------------------------------------------------------"
        echo "$x xxx and zjy are different"
        cat ".\\\\hw10_out\\\\stdout$x.txt" > ".\\\\error1\\\\stderr_xxx_$x.txt"
        cat ".\\\\hw10_out_xxx\\\\stdout$x.txt" > ".\\\\error1\\\\stderr_xxx_$x.txt"
        cat  ".\\\\hw10datas\\\\fileIn$x.txt" >  ".\\\\error1\\\\stderrInput$x.txt"
        echo "------------------------------------------------------------"
        $y=$y+1
    }else {
        echo "$x xxx and xxx are same"
    }
​
if(!$y){
echo "NO_ERROR_THIS_TIME"
echo "Congratulation ! xxx xxx xxx are same in the test of $x"
}

4. 数据构造策略

作业数据构造分为正确性和性能两方面。对于正确性测试,社交关系网络的人数不会影响功能正确性,因此构造少数人的不同情况样例即可。例如,测试第三次作业中的deleteColdEmoji方法:

ap 1 1 1
ap 2 2 2
ar 1 2 1
sei 1
aem 4 1 0 1 2
sm 4
sei 2
aem 5 1 0 1 2
aem 6 1 0 1 2
aem 7 2 0 1 2
dce 1
sm 5
aem 8 1 0 1 2
sm 7

该样例测试了dce方法是否能正确删除heat低于limit的emoji,同时保留heat≥limit的表情消息。

对于性能测试,需要使用包含大量成员的社交网络,利用数据生成器生成随机数据,考虑网络中人数、边的密集程度、指令数量和比例等。不同条件下图的结构也需考虑,例如第三次作业中测试queryLeastMoments时,需要考虑稠密图和稀疏图,否则难以发现Dijkstra算法在稀疏图下表现不佳的问题。

二、架构设计梳理

本单元作业中的图模型基于第一次作业的Block结构,即借鉴并查集的思想,将有关系的人放在一个Block中,维护Block结构,使一个Block中的人都是连通的,从而将qbs等方法的复杂度降到O(1)。具体操作如下:

public void manageBlock(int id1, int id2) {
        HashMap<Integer, Person> block1 = new HashMap<>();
        for (HashMap<Integer, Person> block : blocks) {
            if (block.containsKey(id1)) {
                block1 = block;
                break;
            }
        }
        if (!block1.containsKey(id2)) {
            HashMap<Integer, Person> block2 = new HashMap<>();
            for (HashMap<Integer, Person> block : blocks) {
                if (block.containsKey(id2)) {
                    block2 = block;
                    blocks.remove(block);
                    break;
                }
            }
            block1.putAll(block2);
        }
    }
public void deleteBlock(int id1
​
, int id2) {
        HashMap<Integer, Person> dblock = null;
        for (HashMap<Integer, Person> block : blocks) {
            if (block.containsKey(id1) && block.containsKey(id2)) {
                dblock = block;
                break;
            }
        }
        if (dblock != null) {
            HashMap<Integer, Person> newblock = new HashMap<>();
            getOneBlock(id1, dblock, newblock);
            if (!newblock.containsKey(id2)) {
                dblock.keySet().removeAll(newblock.keySet());
                blocks.add(newblock);
            }
        }
    }

对于其他时间复杂度较高的方法,可以采用“动态维护”的思想,即在改变社交网络的同时维护可能被查询到的数据,查询时直接返回相应数据,时间复杂度降到O(1)。例如,对qts指令,可以在每次加边时判断两点是否与Block中的第三点都有关系来增加qtsum,删边时则需要判断Block中与这两点都有关系的点,减少qtsum。对于qcs指令,因加边或删边影响人数最多是四人,判断这四人bestacquaintance情况即可计算qcsum变化。具体实现如下:

public void manageCouple(int id1, int id2, int oldcp1, int oldcp2) {
        MyPerson person1 = (MyPerson) getPerson(id1);
        MyPerson person2 = (MyPerson) getPerson(id2);
        int newcp1 = person1.getBestAcquaintance();
        int newcp2 = person2.getBestAcquaintance();
        MyPerson new1 = (MyPerson) getPerson(newcp1);
        MyPerson new2 = (MyPerson) getPerson(newcp2);
        MyPerson old1 = (MyPerson) getPerson(oldcp1);
        MyPerson old2 = (MyPerson) getPerson(oldcp2);
        if (oldcp1 != newcp1 || oldcp2 != newcp2) {
            if (newcp1 == id2 && newcp2 == id1) {
                cpsum++;
            }
            if (oldcp1 == id2 && oldcp2 == id1) {
                cpsum--;
            }
            if (newcp1 != oldcp1) {
                if (newcp1 == id2) {
                    if (old1 != null && old1.getBestAcquaintance() == id1) {
                        cpsum--;
                    }
                } else {
                    if (new1 != null && new1.getBestAcquaintance() == id1) {
                        cpsum++;
                    }
                }
            }
            if (newcp2 != oldcp2) {
                if (newcp2 == id1) {
                    if (old2 != null && old2.getBestAcquaintance() == id2) {
                        cpsum--;
                    }
                } else {
                    if (new2 != null && new2.getBestAcquaintance() == id2) {
                        cpsum++;
                    }
                }
            }
        }
    }

三、性能问题及修复

根据JML规格编写代码看似容易,但如果仅依照规格实现,例如使用数组存储Person对象,多次遍历qcs、qts、qlm等,程序运行速度会非常慢,导致强测失败。因此,掌握规格与实现分离的思想至关重要。

规格与实现分离的理念在于它们不同的要求。规格需严谨和易读,采用简单的数据结构和算法,以准确说明所需功能。而实现需保证程序正确性和运行效率。理解JML规格中方法的具体作用、前置条件和限制条件,从程序员角度选择合适的数据结构和算法来实现功能,就是规格与实现分离的具体体现。

在几次作业初期,我未很好地掌握这一思想。例如,第一次作业初步实现未使用动态维护和并查集,而是直接采用规格中的遍历方式,导致程序性能差。在同学建议下,重新修改框架,采用动态维护和并查集,程序效率提高数倍,顺利通过强测。

第三次作业较为复杂,要求查询社交关系网络中的最小环路。起初采用遍历源点每个邻接点,删除边后在新图中使用Dijkstra寻找最短路径,但效率低。后来改用只需运行一次Dijkstra的方法,通过遍历每条边判断最短路径是否重合,找出最小环路,将时间复杂度控制在O(n)内。

即便如此,Dijkstra算法在稀疏图下的高复杂度仍影响程序效率,导致强测CPU_TIME_LIMIT_EXCEED。最终对Dijkstra算法进行堆优化,将更新的最短路径推入堆中,下一次取堆顶元素递归,时间复杂度降低到O((n+m)logn),程序效率提高。

五、学习体会

回顾本单元学习内容,主要目标在于培养根据规格编写代码并进行测试的能力。不同于前两个单元侧重算法的高效性和创新性,本单元更注重算法的严谨性和鲁棒性。

从JML规格书写角度看,我们需确保严谨,准确描述方法的前置条件、副作用和后置条件,保证条件的正确性、完整性和无异议性。必要时构造中间数据,采用共性提取和组合机制等方法优化规格,提高可读性。

从根据JML规格实现代码的角度看,需了解规格管理的数据内容及其操作对象的本质功能,选择适当的数据结构和算法实现功能。

代码实现完成后,需回归规格,依据规格构造测试数据,对代码正确性和性能进行检验。

综上所述,通过本单元学习,我基本熟悉了基于JML规格的项目开发流程,学会了阅读和书写较为严谨的JML规格,同时掌握了依据规格实现代码并进行正确性验证的能力。更为重要的是,领会了规格与实现分离的核心思想,这些技能和思想将在未来的学习和工作中发挥重要作用。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值