BUAA-OO-unit-3-总结
第三单元主题为基于JML规格实现社交网络模拟。通过本单元的学习,我了解到如何进行契约式编程,学习了JML的语法和相应工具链的使用,同时还实践了基于规格的软件测试、单元测试等多种测试方法。
关于测试
测试的策略
在OO课程与软件开发中,测试始终是一门重要的学问。回顾已经过去的三个单元,可以总结出以下几种测试策略:
- 第一单元的测试:主要采用随机生成数据,借助python符号计算包sympy化简表达式进行答案比对的方法,实现较高的测试覆盖率。
- 第二单元的测试:由于不同的答案可能都是正确的,因此无法采用对拍的策略,只能按照电梯的运行规则编写数据检查器,来检验电梯行为的正确性。同样可以利用大量的随机数据得到答案,再通过数据检查器即可。
-
第三单元的测试:本单元作业有如下几个性质:1)得到的答案唯一,可以利用对拍检查;2)给出了方法的规格,能够较好的量化方法的行为,从而实现更细粒度的测试(单元测试),以提升程序的鲁棒性,降低debug的难度。
基于上面的性质,我采用了多层的测试来验证程序的正确性。在类与方法的层面,使用JUNIT进行单元测试;在整个程序的宏观层面上,采用与小伙伴对拍的方式测试。示意如下:
利用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)
-
-
利用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提供的工具查看测试覆盖率:
利用对拍进行全局测试
JUnit能够实现细粒度的单元测试,但是为了使测试数据能够覆盖足够的CornerCase,还是需要通过大量随机数据进行对拍。为此,我维护了本单元作业的随机数据生成器,以及对拍器,其工作流如下图所示:
利用JML工具链实现更高级别的自动化
对于JML这种形式化语言,已经存在一些自动化工具链,如:
-
RunTimeAssertion tool:JMLC
基于已有规格,提供前置条件,正常的后置条件,异常的后置条件,不变式的运行时检查。
-
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类图如下所示:
异常处理
选择使用带计数机制的异常处理:
- 新建计数类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工具链的测试方法,并体会到,不论是静态软件验证还是基于规格的数据自动生成,现有方法都难以满足需求,该领域还有较多的发展空间。
契约式编程相关
- 一些编程中错误的设想,会造成下面的后果...
-
- 函数"绝不会"被那样调用:传递给我的参数总是有效的
- 这段代码肯定会"一直"正常运行:它绝对不会产生错误
- 如果我把这个变量标记为"仅限内部使用",就没有人会试访问这个变量
-
防御式编程 就是为了解决上面的问题。其思想就是,你不能保证任何外部的程序和数据都是正确的。
- 因此,需要利用约束校验(断言),异常和错误处理等手段,保证子程序应该不因传入错误数据而被破坏,并断定代码中每个阶段可能出现的错误,并做出相应的防范措施。
-
契约式编程 吸取了防御式编程的一些优点,同时,避免了其因为assert导致效率较低的情况,我认为其优点如下:
- 相比防御式编程,避免
- 设计与实现相分离,同时契约是一个替代文档的好选择
- 实现功能时,不需要各种异常情况,只需要完善考虑前置条件下的实现即可
- 轻松定位程序出错的位置