[OO] Unit3 Summary JML系列
@
[OO] Unit3 Summary JML系列
JML理论基础
优点
- 对接口行为、功能和规格作出描述和规定,用数学语言描述能够有效确保设计者的本意被清晰表达,没有二义性
- JML将自然语言转化为机器能够理解并执行的语言,这为自动的自动化测试提供支撑
- 理论上,基于规格存在一种完美的自动化测试,能够有效根据JML规格进行自动的数据生成与正确性检验,并且这样的测试是完全基于规格的,因此是完全准确的
语法基础
常用表达式
-
\result表达式:表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。
\result == null;
-
\old( expr )表达式 :用来表示一个表达式 expr 在相应方法执行前的取值。
people.length == \old(people.length);
-
\not_assigned(x,y,...)表达式 :用来表示括号中的变量是否在方法执行过程中被赋值。如果没有被赋值,返回为 true ,否则返回 false
(\forall int i; 0 <= i < groups.length; \not_assigned(groups[i]));
-
\forall表达式 :全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
(\forall int i; 0 <= i < groups.length; \not_assigned(groups[i]));
-
\exists表达式 :存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。
例:(\exists int i; 0 <= i && i < people.length && people[i].getId() == id2;
money[i] == \old(money[i] + value)); -
\sum表达式 :返回给定范围内的表达式的和。
(\sum int i; 0 <= i < people.length && people[i].getAge() >= l && people[i].getAge() <= r; 1);
方法规格
- 前置条件:通过requires子句来表示: requires P 其中requires是JML关键词,表达的意思是“要求调用者确保P为真”。方法规格中可以有多个requires子句,是并列关系,即调用者必须同时满足所有的并列子句要求。
- 后置条件:通过ensures子句来表示,是对方法执行结果的限制,如果执行结果满足后置条件,则表示方法执行正确,方法规格中可以有多个ensures子句,为并列关系,实现者必须同时满足有所并列ensures子句的要求。
- 副作用 :副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。从方法规格的角度,必须要明确给出副作用范围。
- public normal_behavior:方法的正常处理部分。
- public exceptional_behavior:方法的异常处理部分。
- signals:后加表达式,抛出某异常以及抛出异常的条件语句为该表达式,当表达式为真时抛出异常。
- signals_only:强调满足前置条件时候抛出相应异常。
类型规格
- 不变式(invariant):是要求在所有可见状态下都必须满足的特性,语法上定义 invariant P ,其中invariant 为关键词, P 为谓词。
- 状态变化约束(constraint):对前序可见状态和当前可见状态的关系约束,invariant和constraint可以直接被子类继承获得。
JML工具链
不得不说,JML的工具链是真的难用,生态糟糕
-
OpenJML : JML语法检查
-
JMLUnitNG :自动生成测试样例覆盖性测试所有方法,仅使用边界数据
-
其他工具:
ESC/Java2 1, an extended static checker which uses JML annotations to perform more rigorous static checking than is otherwise possible.
Daikon, a dynamic invariant generator.
KeY, which provides an open source theorem prover with a JML front-end and an Eclipse plug-in (JML Editing) with support for syntax highlighting of JML.
Krakatoa, a static verification tool based on the Why verification platform and using the Coq proof assistant.
JMLEclipse, a plugin for the Eclipse integrated development environment with support for JML syntax and interfaces to various tools that make use of JML annotations.
Sireum/Kiasan, a symbolic execution based static analyzer which supports JML as a contract language.
TACO, an open source program analysis tool that statically checks the compliance of a Java program against its Java Modeling Language specification.
VerCors verifier
测试工具使用报告
SMT Solver
- 报错太多,给定的JML需要进行大量修改,此处省略
EvoSuite
- 这是一个较新的自动生成基于Junit的测试代码的工具
- 将evosuite进行部署后,在希望测试的类上点击Run EvoSuite
- 指定生成样例时使用的处理器数量,以及jdk版本和用于生成测试样例的时间后,开始生成
- 得到如下JUnit测试文件,此处仅以MyNetwork为例,其中前两个为EvoSuite自动生成的,后一个是我自己手用JUnit4写的测试
- 测试代码举例如下,演示使用的代码仅生成了66个测试样例,增加生成时间能够提高覆盖率
- 除此之外,其对边界数据的覆盖情况也很好,以test61为代表
- 进行测试后结果如下
- 可以看到,方法覆盖率达到了100%,line覆盖率很高83%,可以结合下文方法查漏补缺,(这个测试文件只是MyNeywork的)
- 下面展示对全部工程文件的测试情况,生成测试代码时指定了使用6个core,CPU占用率基本拉满
- 测试结果和覆盖率如下
- 可以看到覆盖率近乎完美,除了MyNetwork的Line覆盖率83%,对此可以采用提高生成测试代码时间进行弥补,或根据左侧标红(代表未覆盖)进行手动构造测试数据,加入到测试代码中
JMLUnitNG
- 以测试Group.java的实现是否正确为例,执行如下命令
java -jar jmlunitng.jar -cp spec3.jar MyGroup.java
,得到如下文件 - 导入相关依赖包后进行测试,以下是部分测试结果
- 可以看到其主要是针对边界数据进行测试,尤其是NULL的情况
- 以上7个Failures均在null传入参数遇到,相对于EvoSuite而言JMLUnitNG的测试更多是在边界数据,能够找到更多的边界样例,分析其原因可以发现,addPerson和delPerson并没有给出JML规格,所以才会产生NULL的问题
- 将规格进行补全后,所有的测试数据均为Passed
- 对比EvoSuite和JMLUnitNG可以看到,EvoSuite能够产生更多的有意义的实际数据,有些bug是需要这种数量较多才能体现出来的,例如Group中的相关缓存量的更新bug,JMLUnitNG无论如何也测试不出来
- 因此最好的做法是将两者进行结合
黑盒测试
- 最后,笔者同时使用了黑盒测试的方法进行对拍,基于大量复杂数据的测试能够发现一些出现率极低的bug
- 三种测试方法结合使用效果最佳
架构设计
- 采用将最短路单独包装、双连通算法包装、并查集单独包装的外观,各自管理所需的数据结构
- 关于采用的算法与时间复杂度的可行性分析由此可以说明
- JML系列 优化及时间复杂度可行性证明
Bug分析
HW1
- 由于对addRelation的JML规格机械实现,导致其逻辑有所遗漏
- 此处id1 != id2是bug所在,JML中signals的子条件并没有表述完全,而是在大分分支exceptional_behavior下说明的,这一点会导致同在一个连通块下添加Relation的时候会抛出异常
HW2
- 在实现qnr的时候,尝试用java自带的TreeSet进行查询rank
- 但其实name可以有相同的,而TreeSet是不可重复记集合,导致出错
- 修复方法为使用可从重复元素的记集合,或者直接使用遍历的方法实现qnr
HW3
- 在最短路的实现上,常数需要进行严格的控制,使用HashMap和HashSet进行记录大大增加了常数
- 最好的方法是采用数组的方法,因为调用最短路算法时,people的大小已经确定了,所以不需要使用动态的容器,减少扩容和hash计算的时间
- 其次,由于只需要找一个点的最短路径,可以在点出队时候直接剪枝,大大节省时间,因为这时候根据Dijkstra的原理,目标都点的最短路已经不会改变了
心得体会
- 个人感觉JML理论支撑较为成熟,但实际使用上并没有很轻松
- 首先JML规格的书写并不能替代原来写代码时的逻辑思考,而且基于规格的代码保证是正确的,但JML规格的逻辑正确性仍然得不到保证,所做的只是将代码实现的正确性保证转移到了JML规格的书写上
- 其次,JML的工具链使用实在是难以接受,花费了大量时间在JML相关工具链的使用上,而正确性并没有很好地保证,到头来还是需要自动评测机地帮助
- 但另一方面,JML的相关工具还是可以使用的,作为辅助黑盒测试的工具,使用EvoSuite和JMLUnitNG进行边界数据的测试和覆盖性测试
- 最后,这个单元得到一个血的教训,没有亲自做好充分的测试都是不可靠的,总会被感觉所蒙蔽双眼!一切都应该以数据说话!