BUAA-OO-第三单元总结

BUAA-OO-第三单元总结

测试过程

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

黑箱测试和白箱测试是软件测试中的两种基本方法,它们从不同的角度来确保软件的质量和可靠性。概括的来讲,黑箱测试关注外部行为,白箱测试关注内部结构。

  • 黑箱测试

黑箱测试,也称为功能测试或数据驱动测试,是一种不考虑内部结构和代码的测试方法。在黑箱测试中,测试者将被测试的软件视为一个“黑盒子”,只关注其输入与输出,以及系统功能的表现是否符合预期。测试的重点在于验证软件功能是否正确实现,用户界面和业务流程是否按需求工作。对标我们的OO作业,历次强测和第三单元的Junit测试以及流通的各种评测机就可以看作黑箱测试,我们并不关注代码的内部结构,只是提供测试数据,来验证程序的输出是否符合预期。

  • 白箱测试

白箱测试,又称结构测试或逻辑覆盖测试,是一种需要了解并利用软件内部结构和代码逻辑的测试方法。测试者基于对程序代码的详细了解,设计测试用例来验证代码的每条路径、分支、循环等是否都能正确执行。白箱测试的目标是发现编程错误、逻辑错误、安全漏洞等问题,提高代码质量和安全性。举例来说,我们上学期的OOpre课程贯穿始终的Junit测试就可以看作白箱测试,我们需要构造数据来验证程序的每条路径、分支、循环是否正确。

总的来说,黑箱测试验证功能是否符合预期,白箱测试查找编码错误和逻辑问题。结合两种测试方法,可以全面的评估程序的正确性,评判其是否具有良好的结构和稳定性。

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

  • 单元测试

单元测试是对软件中最基本的可测试单元(通常是函数、方法或类)进行验证的过程。它的目的是验证这些单元按照预期工作,独立于它们如何在软件的其他部分中被调用。通过单元测试可以确保被测试的最小单元实现正确,并且能够精准地定位bug,有助于快速定位问题。

  • 功能测试

功能测试,也称为系统测试,关注的是验证整个系统或应用程序是否满足特定的功能需求和规格说明。这种测试类型是从最终用户的角度出发,确保软件提供的功能与需求文档中所定义的一致,而不考虑内部结构。测试方式类似于黑箱测试,不关注代码内部结构,仅验证程序是否能够满足用户的需求,但很难断言程序是否完全没有bug,需要进行进一步测试。

  • 集成测试

集成测试是在单元测试之后进行的,目的是检查不同模块或服务之间的接口是否能够正确协作。它关注的是模块间的交互,验证数据能否正确传递,以及组合后的组件能否作为一个整体按预期工作。集成测试可以通过逐步集成各个模块(自底向上、自顶向下或基于接口的集成)来进行,也可能涉及到解决模块间的依赖关系和冲突。

  • 压力测试

压力测试是一种评估系统在高于正常操作负载条件下的表现和稳定性的测试方法。它旨在发现系统的极限,比如处理能力、响应时间、资源使用情况等,当系统处于高负载或过载状态下时,是否能够保持稳定运行或者在何种条件下会失败。压力测试通过模拟极端的工作负载条件,如大量并发用户访问、大数据量处理等,来检测系统的瓶颈和弱点。历次强测中的部分数据点就可看做压力测试,例如第九次作业的某个数据点,构造了含100个点的完全图后大量执行mr指令,又例如第十次作业中的某个数据点,构造了含100个点的完全图,并一直执行qtvs指令。在这种高复杂度、高压力的测试下,可以验证程序是否能够很好的支持边界情况、极端情况。

  • 回归测试

回归测试是在软件修改后(比如修复缺陷、增加新功能)重新执行之前已通过的测试,以确保现有功能没有因为最近的变更而受到影响。其目的是捕捉由于代码改动可能引入的新错误,保证软件的整体质量不受损害。回归测试可以手动执行,但为了效率通常会使用自动化测试工具和框架,以便在每次构建或发布前快速重复执行测试套件。回归测试对于OO作业来说还是十分重要的,在测试出bug并修复之后,我们必须要确保本次bug修复没有引发新的错误,执行回归测试就可以很好的避免这一情况。

数据构造策略

关于Junit自动化测试的数据点,会在下文详细介绍。我在完成初步代码编写之后,会手捏一些简单数据来验证程序是否具有了基本的功能。如果这一步没有问题,我就会使用评测机进行全面测试。我主要使用的是cxc同学和gpf同学的评测机,我没有详细了解cxc同学生成测试数据的逻辑,通过一些测出我错误的点可以发现主要还是依靠随机数据生成测试点;gpf同学的评测机可以支持调整参数,例如改变图中点的数量、边的数量,这样可以实现全面数据构造。

架构设计

经过三次迭代,第十一次作业具有最完整的结构,因此我将对第十一次作业进行分析。

图模型构建

//MyNetwork
private HashMap<Integer, Person> persons;
//MyPerson
private HashMap<Integer, Person> aquaintance;//具有社交关系的集合
private HashMap<Integer, Integer> value;//对应的权值

我通过Java中的HashMap数据结构来构建了图模型,主要利用了Personid唯一这一性质。MyNetwork中含有所有的Person,每个Person中存储和他具有联系的人的集合aquaintance以及对应边的权值的集合value,同样利用HashMap数据结构。在这种数据结构下,查找复杂度可以降为O(1)。

维护策略

JML虽然已经将每个方法的功能表达清楚,但如果我们每个方法都按照JML进行书写,那么部分方法复杂度会很高,当图十分复杂的时候,就可能引发ctle,因此我们需要维护一些中间变量,采用复杂度更低的算法来规避这个问题。

  • query_circle

这个指令需要判断图中两个点是否连通,第一思路就是利用dfs遍历,但考虑到复杂度问题,我最终采用了并查集进行优化。

//DisjointSet 
private HashMap<Integer, Person> persons;
private HashMap<Integer, Integer> fatherMap;
    public void add(int id) {
        if (!fatherMap.containsKey(id)) {
            fatherMap.put(id, id);
        }
    }
    public int find(int id) {
        int represent = id;
        while (represent != fatherMap.get(represent)) {
            represent = fatherMap.get(represent);
        }
        int now = id;
        while (now != represent) {
            int father = fatherMap.get(now);
            fatherMap.put(now, represent);
            now = father;
        }
        return represent;
    }
    public int merge(int id1, int id2) {
        int father1 = find(id1);
        int father2 = find(id2);
        if (father1 == father2) {
            return -1;
        }
        fatherMap.put(father1, father2);
        return 0;
    }

并查集的思路就是将所有连通的点放在同一个集合中,并以一个对应的元素来代表这个集合,当判断两个点是否连通时,只需判断这两个点所处集合对应的元素是否相同即可。具体就是通过addfindmerge操作来构建并查集。

但需要注意的一点是,当我们通过mr指令删除了图中的一条边,这会导致并查集结构的改变,因此我们还需要解决这个问题,我利用了染色法解决。

public int split(int id1, int id2) {
        fatherMap.put(id2, null);
        dfs(id1, id1, new HashMap<>());
        if (fatherMap.get(id2) == null) {
            dfs(id2, id2, new HashMap<>());
            return 0;
        } else {
            return -1;
        }
    }

    private void dfs(int startId, int newFatherId, HashMap<Integer, Boolean> visited) {
        Stack<Integer> funStack = new Stack<>();
        funStack.push(startId);
        while (!funStack.isEmpty()) {
            int id = funStack.pop();
            if (visited.containsKey(id)) {
                continue;
            }
            visited.put(id, true);
            fatherMap.put(id, newFatherId);
            MyPerson currentPerson = (MyPerson) this.persons.get(id);
            for (Person acquaintance : currentPerson.getAquaintance().values()) {
                int acquaintanceId = acquaintance.getId();
                if (!visited.containsKey(acquaintanceId)) {
                    funStack.push(acquaintanceId);
                }
            }
        }
    }

基本思路就是先将其中一个点1孤立出去,再利用dfs遍历另一个点2,如果点1和点2又产生了联系则返回-1,否则返回0。这里有一个注意的点,dfs应该用栈模拟的方式书写,递归调用会有爆栈的可能。

  • query_block_sum

这个指令需要我们判断当前图的极大连通子图的个数,我在MyNetwork中利用blockSum来维护这个值。思路很简单,每当总图加入一个点,blockSum就加一;每当总图加入一条边,通过对并查集merge方法的返回值进行判断,如果这条边连接的两点本来就连通,那么blockSum不变,否则blockSum减一;每当总图删除一条边,通过对并查集split方法的返回值进行判断,如果这条边连接的两点仍连通,那么blockSum不变,否则blockSum加一。

  • query_triple_sum

这个指令需要我们判断当前图的三元连通子图的个数,我在MyNetwork中利用tripleSum来维护这个值。

每当加入一条边:

for (Integer personId : myPerson1.getAquaintance().keySet()) {
                if (myPerson2.getAquaintance().containsKey(personId)) {
                    tripleSum++;
                }
}

同理每当删除一条边:

for (Integer personId : myPerson1.getAquaintance().keySet()) {
                    if (myPerson2.getAquaintance().containsKey(personId)) {
                        tripleSum--;
                    }
}

tripleSum增加或减少该边连接的两个人的aquiantance的公共元素的个数。

  • query_tag_value_sum

对于这个指令,我在MyTag中利用valueSum来维护这个值。并且在MyPerson中维护了isHadTags,存储该Person所属的tag

//MyTag
private int valueSum;
//MyPerson
private HashSet<Tag> isHadTags;

每当在Tag中加人或删人时,都需要更新valueSum的值。

//addPerson
for (Integer key : myPerson.getValue().keySet()) {
            if (persons.containsKey(key)) {
                valueSum += 2 * myPerson.getValue().get(key);
            }
        }
//delPerson
for (Integer key : myPerson.getValue().keySet()) {
            if (persons.containsKey(key)) {
                valueSum -= 2 * myPerson.getValue().get(key);
            }
        }

其次在MyNetwork中,每当增加一条边或删除一条边时,也要更新valueSum的值。

private void changePublic(HashSet<Tag> tags1, HashSet<Tag> tags2, int value, boolean isAdd) {
        HashSet<Tag> tags = new HashSet<>(tags1);
        tags.retainAll(tags2);
        if (isAdd) {
            for (Tag tag : tags) {
                MyTag myTag = (MyTag) tag;
                myTag.changeValueSum1(value);
            }
        } else {
            for (Tag tag : tags) {
                MyTag myTag = (MyTag) tag;
                myTag.changeValueSum2(value);
            }
        }
    }
  • query_tag_age_var

对于这个指令,需要在MyTag中维护ageSumagePowerSum两个属性,记录tag中所有人年龄之和和年龄平方和,每当tag加人或删人时进行更新,而后需要得到方差时利用这两个值进行计算。

public int getAgeMean() {
        if (persons.isEmpty()) {
            return 0;
        } else {
            return ageSum / persons.size();
        }
    }
public int getAgeVar() {
        if (persons.isEmpty()) {
            return 0;
        } else {
            int ageMean = getAgeMean();
            return (agePowerSum - 2 * ageMean * ageSum + persons.size() * ageMean * ageMean) /persons.size();
        }
    }
  • query_best_acquaintance

这个指令需要得到一个Person的所有与他有联系的人中,value最大且id最小的那个人的id,我在MyPerson中利用TreeMapTreeSet来维护bestId

private TreeMap<Integer, TreeSet<Integer>> bestAcquaintanceMap;

TreeMap的键是value,值是该value对应的所有id,利用现成的数据结构进行排序,可以使复杂度尽可能降低。

性能问题与修复情况

在三次作业中我在强测和互测都没有出现bug,主要还是得益于采用了时间复杂度低的算法和维护中间变量的做法,倘若一味的按照JML进行书写,那么超时的结果是无法避免的。因此我们需要有时候从规格中跳脱出来,但这并不意味着违反规格,只是将规格与实现分离,通过更简单的方法达到相同的目的。

Junit测试

在我看来,Junit测试在本单元主要需要考虑两个方面,一是数据构造,二是逻辑判断。我将以第十次作业为例进行这两方面的分析(第十次作业我的数据生成较为全面)。

数据构造

public void prepareData1() throws EqualPersonIdException, PersonIdNotFoundException, EqualRelationException {
        MyNetwork myNetwork = new MyNetwork();
        Random random = new Random();
        for (int i = 1; i <= 100; i++) {
            myNetwork.addPerson(new MyPerson(i, "num+" + i, i));
            System.out.println("ap "+i+" "+i+" "+i);
        }
        for (int i = 1; i <= 100; i++) {
            for (int j = i + 1; j <= 100; j++) {
                int ifAddRelation = random.nextInt(100);
                if (ifAddRelation >= 50) {
                    int value = random.nextInt(100) + 1;
                    myNetwork.addRelation(i, j, value);
                    System.out.println("ar "+i+" "+j+" "+value);
                }
            }
        }
        tempTest(myNetwork);
    }

以其中一个方法为例,首先我构建了一百个孤立点,接着每两个点间我以50%的概率生成一条边,这样就构造出了一个半完全图,概率大小可调。例如将生成边概率调至90%就可以得到一个稠密图,而调至10%则会得到一个稀疏图。经过我的尝试,评测机对于数据的覆盖情况考察还是比较全面的,因此我生成了不同的图,并测试了较多的组数。

逻辑判断

public void tempTest(MyNetwork myNetwork) {
        Person[] oldPersons = myNetwork.getPersons();
        int answer2 = 0;
        for (int i = 0; i < oldPersons.length; i++) {
            for (int j = i + 1; j < oldPersons.length; j++) {
                try {
                    if (myNetwork.queryBestAcquaintance(oldPersons[i].getId()) == oldPersons[j].getId() && myNetwork.queryBestAcquaintance(oldPersons[j].getId()) == oldPersons[i].getId()) {
                        answer2++;
                    }
                } catch (AcquaintanceNotFoundException | PersonIdNotFoundException ignored) {
                }
            }
        }
        int answer1 = myNetwork.queryCoupleSum();
        assertEquals(answer1, answer2);
        Person[] nowPersons = myNetwork.getPersons();
        assertEquals(oldPersons.length, nowPersons.length);
        for (int i = 0; i < oldPersons.length; i++) {
            MyPerson myPerson1 = (MyPerson) oldPersons[i];
            MyPerson myPerson2 = (MyPerson) nowPersons[i];
            assertTrue(myPerson1.strictEquals(myPerson2));
        }
    }

在逻辑判断方面需要结合具体的JML约束进行书写,我们需要判断后置条件是否满足,即执行完对应方法后是否达到了预期的效果,例如这里的assertEquals(answer1, answer2);就是在判断方法的返回值和我们通过规格要求计算出的值是否一致。对于pure类的方法,我们还需要确保在方法执行前后,所有数据都没有被改动,例如这里的assertTrue(myPerson1.strictEquals(myPerson2));就是在判断这一方面。

心得体会

经过前两个单元的狂轰乱炸,我认为目前的我已经无懈可击了(至少在心态方面)。

首先能明显的感受到难度相比前两个单元降低了不少,主要原因是相比前两单元略去了架构设计这一繁杂的部分,JML已经帮助我们建立好了完整的架构,我们只需要进行代码补全。其次是细节的考量得到了上升,规格本身就是个比较复杂的东西,如果读的时候不仔细,遗漏了部分条件,或者是会错了意,那写出bug就是不可避免的了。最后是一点其他想法,感觉这一单元的主题虽然是契约式编程,学习JML规格,但不论是我还是周围的同学都花了大把的时间在研究算法上,生怕算法复杂度过高导致超时,个人认为这方面精力投入有些过高,希望课程组能进行一些侧重点的调整。

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值