BUAA-OO-unit-3-总结

第三单元主题为基于JML规格实现社交网络模拟。通过本单元的学习,我了解到如何进行契约式编程,学习了JML的语法和相应工具链的使用,同时还实践了基于规格的软件测试、单元测试等多种测试方法。

关于测试

测试的策略

在OO课程与软件开发中,测试始终是一门重要的学问。回顾已经过去的三个单元,可以总结出以下几种测试策略:

  • 第一单元的测试:主要采用随机生成数据,借助python符号计算包sympy化简表达式进行答案比对的方法,实现较高的测试覆盖率。
graph LR subgraph test in U1 C(Generator)--generate data-->B(Tester)--get result-->A C--data-->G(sympy)--res-->A A(Judgee) end
  • 第二单元的测试:由于不同的答案可能都是正确的,因此无法采用对拍的策略,只能按照电梯的运行规则编写数据检查器,来检验电梯行为的正确性。同样可以利用大量的随机数据得到答案,再通过数据检查器即可。
graph LR subgraph test in U2 C(Generator)--generate data-->B(Tester)--get result-->A A(Checker: a FSM) A--shift state-->A end
  • 第三单元的测试:本单元作业有如下几个性质:1)得到的答案唯一,可以利用对拍检查;2)给出了方法的规格,能够较好的量化方法的行为,从而实现更细粒度的测试(单元测试),以提升程序的鲁棒性,降低debug的难度。

    基于上面的性质,我采用了多层的测试来验证程序的正确性。在类与方法的层面,使用JUNIT进行单元测试;在整个程序的宏观层面上,采用与小伙伴对拍的方式测试。示意如下:

graph LR subgraph JUnit C(Unit test)-->B(Overall test) C--testing each module respectfully-->C B--testing the whole program-->B end B-->A(beat matching)

利用JUNIT进行基于规格的单元测试

JUnit is a simple framework to write repeatable tests


  • Junit的工作方式

    • JUnit主要使用Assert检验类与方法的正确性。测试正确,可利用工具查看覆盖率等测试错误,则抛出异常。其工作流如下图所示:

      flowchart LR O(Tester)--generate-->A A(testCase)--data-->B(class to be tested) B-->C(Actual Output) A-->D(Correct Output) subgraph Assert! C-->E(compare) D-->E(compare) end E--true-->G(next TestCase) E--false-->F(Exception)
    • JUnit支持以下几种assert。(注意区分Junit的assert与java原生的assert)

      image

  • 利用JUnit对本单元进行基于规格的单元测试

    • 由于我们已经有了规格,因此可以基于规格设计JUnit的单元测试样例。具体地:

      • 每个前置条件对应两个正例、负例
      • 每个后置条件用assert检查正确性
    • 具体举例:对queryGroupValueSum方法的Junit测试用例:

      	/*@ public normal_behavior
            @ requires (\exists int i; 0 <= i && i < groups.length; groups[i].getId() == id);
            @ ensures \result == getGroup(id).getValueSum();
            @ also
            @ public exceptional_behavior
            @ signals (GroupIdNotFoundException e) !(\exists int i; 0 <= i && i < groups.length;
            @          groups[i].getId() == id);
            @*/
      	@org.junit.Test
          public void queryGroupValueSum() {
              Counter.getInstance().initial();
              MyNetwork myNetwork = new MyNetwork();
              try {
                  myNetwork.addGroup(new MyGroup(1));
              } catch (EqualGroupIdException ignored) {}
              try {
                  myNetwork.addPerson(new MyPerson(1, "a", 11));
                  myNetwork.addPerson(new MyPerson(2, "b", 11));
                  myNetwork.addPerson(new MyPerson(3, "c", 11));
                  myNetwork.addRelation(1, 2, 10);
                  myNetwork.addRelation(2, 3, 20);
                  myNetwork.addToGroup(1, 1);
                  myNetwork.addToGroup(2, 1);
                  myNetwork.addToGroup(3, 1);
              } catch (PersonIdNotFoundException e) {
                  e.print();
              } catch (EqualRelationException e) {
                  e.print();
              } catch (EqualPersonIdException | GroupIdNotFoundException ignored) {
                  //na
              }
              //********positive
              try {
                  int val = myNetwork.queryGroupValueSum(1);
                  assertEquals(val, 60);
              } catch (GroupIdNotFoundException e) {
                  //do nothing
              }
              //********negative
              boolean thrown = false;
              try {
                  myNetwork.queryGroupValueSum(2);
              } catch (GroupIdNotFoundException e) {
                  thrown = true;
              }
              assertTrue(thrown);
      
      
              try {
                  myNetwork.addRelation(1, 3, 10);
                  int val = myNetwork.queryGroupValueSum(1);
                  assertEquals(val, 80);
              } catch (PersonIdNotFoundException | EqualRelationException | GroupIdNotFoundException e) {
                  //na
              }
          }
      
  • IDEA对JUnit有较好的支持,运行测试后,还可以利用IDEA提供的工具查看测试覆盖率:

image

利用对拍进行全局测试

JUnit能够实现细粒度的单元测试,但是为了使测试数据能够覆盖足够的CornerCase,还是需要通过大量随机数据进行对拍。为此,我维护了本单元作业的随机数据生成器,以及对拍器,其工作流如下图所示:

graph LR C(Generator)--generate data-->B(Victim1)--get result-->A C(Generator)--generate data-->dd(Victim2)--get result-->A C(Generator)--generate data-->Bd(Victim3)--get result-->A A(Judger) A--compare between each other-->A

利用JML工具链实现更高级别的自动化

对于JML这种形式化语言,已经存在一些自动化工具链,如:

  • RunTimeAssertion toolJMLC

    基于已有规格,提供前置条件,正常的后置条件,异常的后置条件,不变式的运行时检查。

  • Unit test generator: jmlunitNG

    能根据jml规格,自动生成单元测试,免去程序员手写测试。同时使用JMLC运行时检查判断正确性。

OpenJML集成了上述一些工具,且易于部署,可以用来根据JML生成数据,并按下面的命令静态和动态地检查程序:

//openJML static check
Java jar openjml.jar –esc *.java
//openJML run time assertion check
Java jar openjml.jar –rac *.java

上述方法具有一定局限性:

  • 不论是静态还是动态的检查,速度极慢
  • 程序规模稍大,就很容易崩溃
  • 对与作业规模的程序,难以实现检查

架构设计与社交网络图维护

UML类图如下所示:

image

异常处理

选择使用带计数机制的异常处理:

  • 新建计数类Counter(采用单例模式)。
  • 记录每类异常每个id的出现次数,并用HashMap管理数据。
public class Counter {  
    private static final Counter COUNTER = new Counter();  
    private HashMap<String, HashMap<Integer, Integer>> personWiseCnt;  
    private HashMap<String, Integer> cnt;  
  
    public void initial();  
  
    public static Counter getInstance() { return COUNTER; }  
  
    public int getExcCnt(String exc);  
  
    public int getExcPersonCnt(String exc, Integer id);  
  
    public void newException(String exc, Integer id);  
  
    public void newException(String exc, Integer id1, Integer id2);  
}

图结构的维护策略

新建Graph类维护社交网络关系图:

  • 使图上操作与Network类解耦。
  • 为最小生成树、最短路设置查询方法,有效利用缓存机制提升效率。
public class Graph {  
    private class Edge implements Comparable<Edge> {  
        //...  
    }  
    private class Node implements Comparable<Node> {  
        private Person person;  
        private int dis;  
        //...  
    }  
    getLeastConnection();  
    getDis(Person s, Person t);  
}  

性能与效率问题

  • 关于并查集

    • 可以使用路径压缩加速,注意避免递归。

    • 存在一些情形,使得路径压缩法不能得到一棵层数为2的树,此时使用按秩合并的方式,将深度小的树合并到深度大的下面,可以进一步提升效率:

      if ((root1).getRank() < (root2).getRank()) {  
          //set the parent of root1 to be root2...   
      } else if ((root1).getRank() > (root2).getRank()) {  
          //set the parent of root2 to be root1...   
      } else {  
          //set the parent of root1 to be root2...   
          (root2).setRank((root2).getRank() + 1);  
      } 
      
  • 指令qgvs:增删时维护总和

    • 执行atg,dfg指令时,遍历成员数组增大或减小valueSum

    • 执行ar指令时,遍历groups,增加相应群组的valueSum

      // Group.addPerson  
      for (Person person1 : people.values()) {  
          if (person1.isLinked(person)) {  
              //add 2 * person1.queryValue() to valueSum
          }  
      }  
      //Network.addRelation  
      for (Group group : groups.values()) {  
          if (group.hasPerson(getPerson(id1)) &&   
                  group.hasPerson(getPerson(id2))) {
              //add 2 * person1.queryValue() to group.valueSum
          }  
      } 
      
      
  • 指令qgam、qgav

    • 维护元素和ageSum,平方和ageSumPwr

    • 注意整除操作的顺序

      public int getAgeVar() {  
              return ageSumPwr - 2 * this.getAgeMean() * ageSum +  
                      people.size() * this.getAgeMean() * this.getAgeMean();  
      } 
      

BUG分析

  • 自己程序的BUG 三次公测,互测中均未出现bug。
  • 自测过程中出现过的问题
    • 指令qgvs:增删时维护总和时,忽略了ar指令的影响,没有在ar时遍历groups,增加相应群组的valueSum。

Network拓展JML规格

  • 新增 Producer,Customer,Advertiser接口,继承自Person接口,用于管理生产者、消费者、发送广告;ProductMessage接口继承自Messege接口,用于管理产品相关信息。

  • 为Producer的produce方法,Customer的consume方法,和Advertiser的advertise方法编写规格:

    /*@ public normal_behaviour
      @ requires !producer.hasProduct(product);
      @ assignable producer.products;
      @ ensures producer.products.length = \old(producer.products.length) + 1;
      @ ensures producer.hasProduct(product);
      @ ensures (\forall int i; 0 <= i && i < \old(producer.products.length);
      @         (\exists int j; 0 <= j && j < producer.products.length;
      @			producer.products[j] == (\old(producer.products[i]))));
      @ also 
      @ public exceptional_behaviour
      @ signals (DuplicatedProductException e) producer.hasProduct(product);
      @*/
    void produce(Producer producer, ProductMessage product);
     
    /*@ public normal_behaviour
      @ requires !advertiser.hasProduct(product) && producer.hasProduct(product);
      @ assignable advertiser.products;
      @ ensures advertiser.products.length = \old(advertiser.products.length) + 1;
      @ ensures advertiser.hasProduct(product);
      @ ensures (\forall int i; 0 <= i && i < \old(advertiser.products.length);
      @         (\exists int j; 0 <= j && j < advertiser.products.length;
      @               advertiser.products[j] == (\old(advertiser.products[i]))));
      @ also 
      @ public exceptional_behaviour
      @ signals (ProductNotFoundException e) !producer.hasProduct(product);
      @ also 
      @ public exceptional_behaviour
      @ signals (DuplicatedProductException e) producer.hasProduct(product) && advertiser.hasProduct(product);
      @*/
    void advertise(Producer producer, Advertiser advertiser, ProductMessage product);
     
    /*@ public normal_behaviour
      @ requires advertiser.hasProduct(product) && !customer.hasProduct(product);
      @ assignable customer.products, advertiser.products;
      @ assignable customer.money, product.getProducer().money;
      @ ensures customer.products.length = \old(customer.products.length) + 1;
      @ ensures customer.hasProduct(product);
      @ ensures (\forall int i; 0 <= i && i < \old(customer.products.length);
      @         (\exists int j; 0 <= j && j < customer.products.length;
      @               customer.products[j] == (\old(customer.products[i]))));
      @ ensures advertiser.products.length = \old(advertiser.products.length) - 1;
      @ ensures !advertiser.hasProduct(product);
      @ ensures (\forall int i; 0 <= i && i < advertiser.products.length;
      @         (\exists int j; 0 <= j && j < \old(advertiser.products.length);
      @               advertiser.products[i] == (\old(advertiser.products[j]))));
      @ ensures customer.money = customer.money - product.getPrice();
      @ ensures product.getProducer().money = product.getProducer().money + product.getPrice();
      @ also 
      @ public exceptional_behaviour
      @ signals (ProductNotFoundException e) !advertiser.hasProduct(product);
      @ also 
      @ public exceptional_behaviour
      @ signals (DuplicatedProductException e) advertiser.hasProduct(product) && customer.hasProduct(product);
      @*/
    void purchase(Advertiser advertiser, Customer customer, ProductMessage product);
    

体验与心得:测试和契约式编程

测试相关

  • 通过本单元的学习,我总结了之前作业的测试策略,并实践了JUNIT单元测试的新方式。本次作业让我意识到,测试不仅仅在数据上可以构造层次,在测试的方法、测试的粒度上也可以分层:可以针对单元构造测试,也可以把软件当成一个整体去测试;这些测试形式可以互相作为补充,更好的为软件设计服务。
  • 我对软件测试有了更深入的了解,并且启发我在今后的学习工作中更加重视测试。
  • 我还了解了基于JML工具链的测试方法,并体会到,不论是静态软件验证还是基于规格的数据自动生成,现有方法都难以满足需求,该领域还有较多的发展空间。

契约式编程相关

  • 一些编程中错误的设想,会造成下面的后果...

image

    • 函数"绝不会"被那样调用:传递给我的参数总是有效的
    • 这段代码肯定会"一直"正常运行:它绝对不会产生错误
    • 如果我把这个变量标记为"仅限内部使用",就没有人会试访问这个变量
  • 防御式编程 就是为了解决上面的问题。其思想就是,你不能保证任何外部的程序和数据都是正确的。

    • 因此,需要利用约束校验(断言)异常和错误处理等手段,保证子程序应该不因传入错误数据而被破坏,并断定代码中每个阶段可能出现的错误,并做出相应的防范措施。
  • 契约式编程 吸取了防御式编程的一些优点,同时,避免了其因为assert导致效率较低的情况,我认为其优点如下:

    • 相比防御式编程,避免
    • 设计与实现相分离,同时契约是一个替代文档的好选择
    • 实现功能时,不需要各种异常情况,只需要完善考虑前置条件下的实现即可
    • 轻松定位程序出错的位置
    • image
posted @ 2022-06-06 00:09  gnwekge  阅读(65)  评论(0编辑  收藏  举报