BUAA_OO_第三单元总结

BUAA_OO_第三单元总结

一、测试过程

1.1 黑盒测试和白盒测试

  • 黑盒测试又称为功能测试,主要检测软件的每一个功能是否能够正常使用。在测试过程中,将程序看成不能打开的黑盒子,不考虑程序内部结构和特性的基础上通过程序接口进行测试,检查程序功能是否按照设计需求以及说明书的规定能够正常打开使用。
  • 白盒测试也称为结构测试,主要用于检测软件编码过程中的错误。程序员的编程经验、对编程软件的掌握程度、工作状态等因素都会影响到编程质量,导致代码错误。

下面是在网上找的一个图:简单的来说就是黑盒测试只关注系统的输入和输出,而不考虑内部的实现细节;而白盒测试需要去了解具体实现,目的是发现系统中的逻辑错误、代码缺陷和性能问题等。

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

  • 单元测试是针对软件的最小功能模块进行测试的方法。在这种测试下,会针对每一个函数进行测试,以确保它们共同完成任务时,能够不发生错误,比如我们这几次作业中的针对一个函数的Junit测试。
  • 功能测试是对整个软件系统的功能进行测试的方法,功能测试的目的是确保软件能够按照用户需求的要求正常运行,并能够正确地处理各种情况。
  • 集成测试在单元测试之后,将各个功能模块组装在一起进行测试的方法,确保各个模块之间的集成能够正常工作,不会产生冲突和错误。
  • 压力测试在高负载和大并发情况下进行测试的方法。测试时增加系统负载,目的是找出系统的瓶颈和性能问题,并且确定系统在高负载情况下的性能指标。
  • 回归测试在代码进行修改或升级后,重新运行之前的测试用例以验证修改是否引入新的错误或导致原有功能出现问题的方法目的是确保原有的功能和性能没有受到影响。

1.3 数据构造策略

虽然我没有自己写过测评机,但是在Junit中也是手动构造了一些数据,在不能通过后,又写了一些随机数据。在数据构造时,针对所有的指令集构造大量随机数据,可以测试正确性;但是在看过一些强测数据时,发现一些数据点完全是针对很少的指令生成针对性数据,特别是一些费时的查询指令,这种方法可以测试程序是否具有一定的性能。

二、架构设计

这几次作业的结构都比较简单,主要就是针对MyNetwork里面的方法的算法的优化比较花费时间和精力。

2.1 第一次作业

第一次作业中,我主要采用并查集的方法进行优化性能,优化后的并查集算法主要有以下几个方法:

public void add(int id) {
        if (!pre.containsKey(id)) {
            pre.put(id, id);//初始化阶段,直接让一个节点的父节点是自己
            rank.put(id, 0);
            blocksum++;
        }
    }

    public int find(int id) {
        int root = id;
        while (root != pre.get(root)) {
            root = pre.get(root);
        }
        //上面保证了root是id的根节点

        int now = id;
        while (now != root) {
            int fa = pre.get(now);
            pre.put(now, root);
            now = fa;
        }
        //上面实现了将路径上所有元素的最顶级的那个元素 设置为 这条路上所有元素的父节点
        return root;
    }

    public int merge(int id1, int id2) {
        int fa1 = find(id1);
        int fa2 = find(id2);
        if (fa1 == fa2) {
            return -1;
        }
        int rank1 = rank.get(fa1);
        int rank2 = rank.get(fa2);
        if (rank1 < rank2) {
            pre.put(fa1, fa2);
        }
        else {
            if (rank1 == rank2) {
                rank.put(fa1, rank1 + 1);
            }
            pre.put(fa2, fa1);
        }
        find(id1);
        find(id2);
        blocksum--;
        return 0;
    }

MyNetwork里面需要特别注意的几个方法是:

  1. isCircle(int id1, int id2):判断两个节点之间是否有路径,在本次作业中,我采用的是优化过的并查集算法,所以只需要一部判断即可:
			//当不抛出异常时,该函数内部只需简化为这一步:
			if (set.find(id1) == set.find(id2)) {
                return true;
            }
            return false;
  1. queryBlockSum():该方法是查询这个图有几个分支,在采用并查集的算法后,该方法并不需要进行很复杂的操作,因为这个参数是在并查集建立时不断维护的。
    具体维护算法为:当并查集添加一个点时:blocksum++;当merge成功时,blocksum–。但是并查集天生对删除关系不友好,所以一旦要删除关系,再次进行查询时,就得进行并查集的重建。但是在这儿我们采用延迟重建的方式,用脏位维护,当需要查询时并且脏位为一,就需要重建后再查询。

  2. queryTripleSum():该方法是查询这个图有几个类似于甲、乙、丙都互相认识的关系的小团体,这个参数也是动态维护的。
    具体维护算法为:

  • 当新添加一个关系时:
			MyPerson p1 = persons.get(id1);
            MyPerson p2 = persons.get(id2);
            for (Integer key : persons.keySet()) {
                MyPerson p = persons.get(key);
                if (p.getId() != id1 && p.getId() != id2) {
                    if (p.isLinked(p1) && p.isLinked(p2)) {
                        set.addtriplesum();
                    }
                }
            }
  • 当删除一个关系时:
			for (Integer kk : persons.keySet()) {
                MyPerson pp = persons.get(kk);
                if (kk != id1 && kk != id2 && a1.isLinked(pp) && a2.isLinked(pp)) {
                    set.subtriplesum();
                }
            }

这个算法虽然是在并查集里维护的,但是并不依靠并查集的结构来查询,所以重建并查集时,并无需改动TripleSum这个参数。

2.2 第二次作业

第二次作业主要是添加了一个MyTag类和其他几个异常类以及一些方法。主要需要用到优化的地方主要是MyNetwork类内新添加的几个方法和MyTag类的几个方法:
MyNetwork类内的方法:

  1. queryBestAcquaintance(int id):找到一个节点的关系值最大的id最小的节点。
    这个方法必须要进行维护,只是简单的进行遍历会导致性能太低,我在这里采用TreeMap的算法:将value当作key,每一个value也是一个TreeSet,里面的适合这个人的认识的人的id的从小到大的排列,在增删改的过程中分别进行维护。
private TreeMap<Integer, TreeSet<Integer>> value2id = new TreeMap<>(new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            int result = o2 - o1;
            return result;
        }
    });
  1. queryCoupleSum():若两个人互为BestAcquaintance,则算作一个Couple,找到Couple的个数。
    这个方法要注意采用这种二重循环,而不是对所有人的二重循环,否则会导致性能过差。
	public int queryCoupleSum() {
        int result = 0;
        for (Integer key : persons.keySet()) {
            if (!persons.get(key).gethashmap().isEmpty()) {
                int partner = persons.get(key).getbestacquaintance();
                if (key == persons.get(partner).getbestacquaintance()) {
                    result++;
                }
            }
        }
        return result / 2;
    }
  1. queryShortestPath():两个节点之间的最短路径,但是我也不知道为什么,和实际生活中的最短路径不太一样,如果实际路径长度为,返回值应该是l-1。
    采用最常见的BFS算法:
public int Bfs_Min_Distance(int u, int aim) {
        HashMap<Integer, Integer> d = new HashMap<>();
        HashMap<Integer, Boolean> visited = new HashMap<>();
        //d[i]表示从u到i结点的最短路径
        for (Integer key : persons.keySet())
        {
            d.put(key, -1); //初始化路径长度
            visited.put(key, false);
        }

        Queue<Integer> queue = new LinkedList<>();

        d.put(u, 0);
        visited.put(u, true);
        queue.add(u);
        while (!queue.isEmpty())//BFS算法主过程
        {
            int v = queue.poll(); //队头元素u出队
            HashMap<Integer, Person> acquaintance = ((MyPerson) getPerson(v)).gethashmap();
            for (Integer w : acquaintance.keySet()) {
                if (!visited.get(w)) {
                    d.put(w, d.get(v) + 1);
                    visited.put(w, true);
                    queue.add(w);
                }
                if (visited.get(aim)) {
                    break;
                }
            }
            if (visited.get(aim)) {
                break;
            }
        }
        return d.get(aim);
    }

MyTag类的方法:

  1. getValueSum():这个方法是找一个Tag内所有人之间的关系和的2倍。
    在遍历时,我一开始使用的 O(n^2)的算法,遍历了两遍所有 people集合中的人,查它们是否 isLinked(),该做法并不是最优的。而且在强测和互测中会被hack到!!!在修改为:外层循环遍历 people 集合中的所有人,内层循环历外层循环遍历 person "所有 acquaintance后,能够成功通过bug修复。这是由于整个人群集合是一定非常巨大的,但是每个人能认识的熟人却是相对极小的,因此修改后的方法会变成0(nlogn)的时间复杂度。
		int out = 0;
        for (Integer key1 : persons.keySet()) {
            HashMap<Integer, Person> ac = ((MyPerson) persons.get(key1)).gethashmap();
            for (Integer key2 : ac.keySet()) {
                if (hasPerson(ac.get(key2))) {
                    out += persons.get(key1).queryValue(persons.get(key2));
                }
            }
        }
        return out;

2.3 第三次作业

这一次作业主要是新加了几个Message类:MyEmojiMessage、MyNoticeMessage、MyRedEnvelopeMessage以及新增的几个方法。这次新增的几个方法并没有很多的算法性能要求,但是JML语言的ensures特别多,需要进行很麻烦的操作,只需根据JML语言写即可。

2.4 规格与实现关系

在一开始时,会觉得一些函数完整的把所有的步骤都已经写出来了,那就完全可以照着步骤去写,但是后来再看了学长的博客以后,发现并不能简单的根据规格去写代码,会导致性能问题,也就是说,规格和实现是分离的!!!
下面是一个例子:

	/*@ ensures \result ==
      @         (\sum int i; 0 <= i && i < persons.length &&
      @         (\forall int j; 0 <= j && j < i; !isCircle(persons[i].getId(), persons[j].getId()));
      @         1);
      @*/
    public /*@ pure @*/ int queryBlockSum();

上面这一段是MyNetwork类的一个方法的规格,但是我们在具体实现时,并不是完全按照这个规格来实现的,是按照并查集维护的方法来实现的,上文中已经写过具体算法。

三、bug及性能问题及其修复

  • 在三次作业中唯一一次在强测中遇到的bug就是第二次作业中的Tag类的getValueSum()方法,遍历了两遍所有 people集合中的人,查它们是否 isLinked(),这个方法会导致超时,在修改为:外层循环遍历 people 集合中的所有人,内层循环历外层循环遍历 person "所有acquaintance后,能够成功通过bug修复。

四、有关Junit测试

这一单元的Junit测试真的是太复杂了,第一次接触差点把自己的机会耗尽。还是在写了数据生成以后循环多次测验,而且要保证各种情况都要覆盖到。

4.1 Junit测试设计实现

下面是第三次作业中针对deleteColdEmoji()函数的测试的生成数据的一部分,采用生成两个一模一样的onetwork和network,以防出现某些深拷贝和假拷贝的问题。

			MyNetwork onetwork = new MyNetwork();
            MyNetwork network = new MyNetwork();
            int min = 20;
            int max = 30;
            Random random = new Random(100);
            int size = random.nextInt(max - min + 1) + min;
            HashSet<Integer> hashset = new HashSet<>();
            int maxNode = size * 10;//点的最大编号是多少,可根据需求调参
            while (hashset.size() != size) {
                hashset.add(random.nextInt(maxNode));
            }
            ArrayList<Integer> idlist = new ArrayList<>(hashset);

            //生成其他数据

            ArrayList<Integer> agelist = new ArrayList<>();
            while (agelist.size() < 50) {
                int a = random.nextInt(150) + 1;
                agelist.add(a);
            }
            
            //加人
			for (int i = 0; i < size; i++) {
                try {
                    onetwork.addPerson(new MyPerson(idlist.get(i), namelist.get(i),
                            agelist.get(i)));
                    network.addPerson(new MyPerson(idlist.get(i), namelist.get(i),
                            agelist.get(i)));
                } catch (EqualPersonIdException e) {
                }
            }
            
            //加关系
            for (int i = 0;i < size * 9;i++) {
                int temp1 = random.nextInt(size);
                int temp2 = random.nextInt(size);
                int id1 = idlist.get(temp1);
                int id2 = idlist.get(temp2);
                try {
                    onetwork.addRelation(id1, id2, relationlist.get(i % 500));
                    network.addRelation(id1, id2, relationlist.get(i % 500));
                } catch (PersonIdNotFoundException e) {
                } catch (EqualRelationException e) {
                }
            }
            
            //加Tag
            //加消息
            //发消息
            //判断正确性

4.2 Junit检验代码实现与规格的一致性

针对相应的JML规格,生成相应的代码来检验正确性,实例如下,deleteColdEmoji的部分JML语言以及测试正确性:

	  //针对以下JML语言
	  @ ensures (\forall int i; 0 <= i && i < \old(emojiIdList.length);
      @          (\old(emojiHeatList[i] >= limit) ==>
      @          (\exists int j; 0 <= j && j < emojiIdList.length; emojiIdList[j] == \old(emojiIdList[i]))));
      
      //意思是:所有使用次数大于等于limit的emojiId都不被删除,检验代码如下:
      		for (int i = 0;i < oemojiidlist.length && oemojiidlist[i] != 0;i++) {
                if (oemojiheatlist[i] >= limit) {
                    correctemojiidlist[idheatnum] = oemojiidlist[i];
                    correctemojiheatlist[idheatnum] = oemojiheatlist[i];
                    idheatnum++;
                }
            }
            //下面只需比较correctemojiidlist与network.deleteColdEmoji(limit)后得到的emojiIdlist即可

上面仅仅是针对其中一句JML语言的测试,其他测试类似,用来检验代码实现与规格的一致性。

五、学习体会

  • 这一单元主要是要求去理解规格化语言,在一些规格化语言描述好的方法,其实有些照着他的代码实现就可以,一些方法按照规格可以直接实现,但是另外一些有性能要求的方法里,并不能直接按照规格实现。
  • 对于一些有性能要求的方法,其实考察的主要是那几个算法,在经历过这个单元以后,我对几乎不怎么了解的BFS、并查集等等讲点算法都有了一定程度的理解。
  • 在第二、三次作业中,明显感觉到Junit的实现难度和复杂度其实并不比那些方法要小,需要考虑全面检验条件,包括全面生成数据。
  • 18
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值