JML——OO总结作业第三弹
目录
- 一、JML概述
- 理论基础
- JML工具链
- 二、JML工具链的使用
- JUnit
- openJML & JUnitNG
- 三、作业架构及bug分析
- 四、心得体会
一、JML概述
1.理论基础
- 原子表达式
|
|
---|---|
|
|
|
|
|
|
|
|
|
|
- 量化表达式
|
|
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- 操作符
|
|
---|---|
|
|
|
|
- 方法规格
|
|
---|---|
|
|
|
|
|
|
|
|
- 类型规格
|
|
---|---|
|
|
|
|
2.JML工具链
- JUnit
- OpenJML & JUnitNG
具体使用方法详见第二部分。
二、JML工具链的使用
1. JUnit
JUnit是由 Erich Gamma 和 Kent Beck 编写的一个回归测试框架(regression testing framework)。Junit测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(What)的功能。Junit是一套框架,继承TestCase类,就可以用Junit进行自动测试了。(摘自百度百科)
就我个人的感受而言,JUnit是一个可以实现细粒度代码测试的工具,通过编写测试代码,我们可以有针对性的对每个方法进行测试,从而分别验证每个方法的正确性。以下我以Group接口的实现类MyGroup为待测试类,构建了如下测试代码:
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import test.com.oocourse.spec3.main.Person;
import java.math.BigInteger;
import java.util.HashMap;
import static org.junit.Assert.assertEquals;
public class MyGroupTest {
private MyGroup myGroup;
@Before
public void setUp() throws Exception {
System.out.println("------ Begin MyGroupTest ------");
myGroup = new MyGroup(6);
}
@After
public void tearDown() throws Exception {
System.out.println("------ End MyGroupTest ------\n");
}
@Test
public void getId() {
int id = myGroup.getId();
assertEquals(id, 6);
}
@Test
public void testEquals() {
boolean equal = myGroup.equals(myGroup);
assertEquals(equal, true);
equal = myGroup.equals(new MyGroup(73));
assertEquals(equal, false);
}
@Test
public void addPerson() {
MyPerson myPerson = new MyPerson(513, "Zhu", new BigInteger("1"), 20);
myGroup.addPerson(myPerson);
HashMap people = new HashMap<>();
people.put(513, myPerson);
assertEquals(people, myGroup.getPeople());
}
@Test
public void hasPerson() {
MyPerson myPerson = new MyPerson(513, "Zhu", new BigInteger("1"), 20);
myGroup.addPerson(myPerson);
assertEquals(myGroup.hasPerson(myPerson), true);
assertEquals(myGroup.hasPerson(
new MyPerson(315, "uhZ", new BigInteger("1"), 2)), false);
}
@Test
public void getRelationSum() {
MyPerson myPerson1 = new MyPerson(513, "Zhu", new BigInteger("1"), 20);
MyPerson myPerson2 = new MyPerson(315, "uhZ", new BigInteger("1"), 20);
MyPerson myPerson3 = new MyPerson(135, "huZ", new BigInteger("1"), 20);
MyPerson myPerson4 = new MyPerson(153, "hZu", new BigInteger("1"), 20);
MyPerson myPerson5 = new MyPerson(531, "Zuh", new BigInteger("1"), 20);
myPerson1.addAcquaintance(myPerson2, 3);
myPerson2.addAcquaintance(myPerson1, 3);
myPerson1.addAcquaintance(myPerson4, 5);
myPerson4.addAcquaintance(myPerson1, 5);
myPerson2.addAcquaintance(myPerson3, 5);
myPerson3.addAcquaintance(myPerson2, 5);
myGroup.addPerson(myPerson1);
myGroup.addPerson(myPerson2);
myGroup.addPerson(myPerson3);
myGroup.addPerson(myPerson4);
myGroup.addPerson(myPerson5);
assertEquals(myGroup.getRelationSum(), 11);
}
@Test
public void getValueSum() {
MyPerson myPerson1 = new MyPerson(513, "Zhu", new BigInteger("1"), 20);
MyPerson myPerson2 = new MyPerson(315, "uhZ", new BigInteger("1"), 20);
MyPerson myPerson3 = new MyPerson(135, "huZ", new BigInteger("1"), 20);
MyPerson myPerson4 = new MyPerson(153, "hZu", new BigInteger("1"), 20);
MyPerson myPerson5 = new MyPerson(531, "Zuh", new BigInteger("1"), 20);
myPerson1.addAcquaintance(myPerson2, 3);
myPerson2.addAcquaintance(myPerson1, 3);
myPerson1.addAcquaintance(myPerson4, 5);
myPerson4.addAcquaintance(myPerson1, 5);
myPerson2.addAcquaintance(myPerson3, 5);
myPerson3.addAcquaintance(myPerson2, 5);
myGroup.addPerson(myPerson1);
myGroup.addPerson(myPerson2);
myGroup.addPerson(myPerson3);
myGroup.addPerson(myPerson4);
myGroup.addPerson(myPerson5);
assertEquals(myGroup.getValueSum(), 26);
}
@Test
public void getConflictSum() {
MyPerson myPerson1 = new MyPerson(513, "Zhu", new BigInteger("61"), 20);
MyPerson myPerson2 = new MyPerson(315, "uhZ", new BigInteger("21"), 20);
MyPerson myPerson3 = new MyPerson(135, "huZ", new BigInteger("24"), 20);
MyPerson myPerson4 = new MyPerson(153, "hZu", new BigInteger("26"), 20);
MyPerson myPerson5 = new MyPerson(531, "Zuh", new BigInteger("11"), 20);
myGroup.addPerson(myPerson1);
myGroup.addPerson(myPerson2);
myGroup.addPerson(myPerson3);
myGroup.addPerson(myPerson4);
myGroup.addPerson(myPerson5);
assertEquals(myGroup.getConflictSum(), new BigInteger("33"));
}
@Test
public void getAgeMean() {
MyPerson myPerson1 = new MyPerson(513, "Zhu", new BigInteger("1"), 20);
MyPerson myPerson2 = new MyPerson(315, "uhZ", new BigInteger("1"), 10);
MyPerson myPerson3 = new MyPerson(135, "huZ", new BigInteger("1"), 30);
MyPerson myPerson4 = new MyPerson(153, "hZu", new BigInteger("1"), 40);
MyPerson myPerson5 = new MyPerson(531, "Zuh", new BigInteger("1"), 50);
myGroup.addPerson(myPerson1);
myGroup.addPerson(myPerson2);
myGroup.addPerson(myPerson3);
myGroup.addPerson(myPerson4);
myGroup.addPerson(myPerson5);
assertEquals(myGroup.getAgeMean(), 30);
}
@Test
public void getAgeVar() {
MyPerson myPerson1 = new MyPerson(513, "Zhu", new BigInteger("1"), 20);
MyPerson myPerson2 = new MyPerson(315, "uhZ", new BigInteger("1"), 10);
MyPerson myPerson3 = new MyPerson(135, "huZ", new BigInteger("1"), 30);
MyPerson myPerson4 = new MyPerson(153, "hZu", new BigInteger("1"), 40);
MyPerson myPerson5 = new MyPerson(531, "Zuh", new BigInteger("1"), 50);
myGroup.addPerson(myPerson1);
myGroup.addPerson(myPerson2);
myGroup.addPerson(myPerson3);
myGroup.addPerson(myPerson4);
myGroup.addPerson(myPerson5);
assertEquals(myGroup.getAgeVar(), 200);
}
@Test
public void delPerson() {
MyPerson myPerson1 = new MyPerson(513, "Zhu", new BigInteger("1"), 20);
MyPerson myPerson2 = new MyPerson(315, "uhZ", new BigInteger("1"), 20);
MyPerson myPerson3 = new MyPerson(135, "huZ", new BigInteger("1"), 20);
myGroup.addPerson(myPerson1);
myGroup.addPerson(myPerson2);
myGroup.addPerson(myPerson3);
myGroup.delPerson(myPerson2);
HashMap people = new HashMap<>();
people.put(513, myPerson1);
people.put(135, myPerson3);
assertEquals(people, myGroup.getPeople());
}
}
在上述代码中,我针对MyGroup类中所有继承自Group接口的方法都进行了测试,通过断言与我希望得到的结果进行了比对。经过运行后,我们可以得到如下测试结果:
可以看到,全部方法都通过了测试,标准输出也没有异常输出出现。
2. OpenJML & JUnitNG
- 在给出具体的实现之前,我实在忍不住吐槽一下,JUnitNG真的是太过老旧了……
\sum, \exists, \old
等很多JML语句和语法都不支持,所以我最开始试图直接对作业中的类进行测试的时候,果不其然,疯狂报错。于是,在经过亿点思考之后,我决定放弃……
我最终的做法是自己写了一个很简单的Demo类,并用OpenJML和JUnitNG对其进行了自动化的测试,Demo类的代码如下:
package demo;
public class Demo {
private/*@ spec_public @*/ int val;
public Demo() {
val = 0;
}
/*@ public normal_behavior
@ requires value < 0;
@ assignable \nothing;
@ also
@ public normal_behavior
@ requires value >= 0;
@ assignable val;
@ ensures val == value;
@*/
public void setVal(int value) {
if (value >= 0) {
val = value;
}
}
public int getVal() {
return val;
}
/*@ public normal_behavior
@ ensures \result == (val == value);
@*/
public boolean valEquals(int value) {
return val == value;
}
public static void main(String[] args) {
return;
}
}
以上代码的存储路径为ProjectName\src\demo\Demo.java
,然后将openjml.jar以及jmlunitng.jar拷贝到ProjectName/src
目录下,并在IDEA的控制台输入以下指令:
javac -cp jmlunitng.jar demo/Demo.java
openjml -rac demo/Demo.java
javac -cp jmlunitng.jar demo/Demo_InstanceStrategy.java
java -cp jmlunitng.jar demo.Demo_JML_Test
完成后,我们就可以得到如下测试结果:
从测试样例中,我们可以看到,JUnitNG会倾向于测试边界数据。在某种程度上,这样的测试思路确实有较大可能hack成功,但相应的,我们也可以结合这样的测试样例再从其他测试角度自行补充测试样例。在自动化测试的基础上进行人工测试,这样大概会有更好的测试效果。
三、作业架构及bug分析
1. 作业架构
- 在拿到本单元第一次作业的时候,我的内心其实是很快乐的,毕竟JML形式化规格在很多情况下就是一种直接的代码实现方式。而且只要熟悉离散数学的谓词逻辑表达,理解规格本身也不是很复杂。
- 然而,这三次作业,我竟然有两次没有进入互测……
(心情复杂.jpg) - 现在来看,造成这一结果的最大原因就是我自己对于JML的理解有很大问题。直到第二次作业,我都以为代码实现应该尽可能依照JML规格来进行,至多在算法和容器选择方面可以有些出入。至于结果嘛,满屏的CPU_TLE就是答案了……现在想想,这样的想法也真的是naive。
- 我对JML的理解偏差在三次作业的结构上就有着很明显的体现。如下图所示,我的三次代码都是完全按照指导书和规格所写的那样,针对每个接口分别实现一个类,并在这一个类之内完成接口的所有方法和功能实现。
- 这样的结构很明显的暴露出了一些问题:
- 很明显,没有使用并查集,在算法上并不出色。
- 在结构层次上过于简单,导致类非常的臃肿复杂,特别是
MyNetwork
类,诸如Dijkstra这样的算法实现也扔在了里面,使得类的逻辑比较复杂,复杂度也很高。
2. bug分析
在我的所有失分中,CPU_TLE占了起码80%……简单来说,就是性能爆炸。
要想提升性能,有两个角度可以进行考虑——算法&数据存储。以下我就从这两个角度来分析一下我的代码:
- 在算法角度,我并没有选择时间复杂度最低的方式。无论是isCircle的天花板算法Union-Find还是割点的天花板算法Tarjan,我都没有进行足够的了解(Tarjan确实是没看懂),最终也都退而求其次,采用了dfs等替代算法。
- 在数据存储的角度,我确实试图通过这方面的改进弥补自己在算法方面的差距。但从结果的角度而言,我似乎又玩火了……为了简化isCircle方法的实现开销,我试图在MyPerson内部记录其可达集合,并在每次addRelation的时候进行更新。但由于最终的强测代码中addRelation指令数较大,最后也导致了一定数量的CPU_TLE。
所以,在上述两个方面,我都没有很好的完成,最后的结果果然也就是当场去世了……
四、心得体会
- 本单元的学习整体来说收获还是不小的,如果说前两单元涉及了有关鲁棒性的防御性编程的话,本单元的JML就是非常纯粹的契约式编程思想了。不过,在本单元的作业中,我们更多地是在完成代码实现的工作,而JML编写本身应当是属于代码设计的部分,这一部分同样也需要花费大量的精力,最终呈现出足够严禁且满足需求的规格化描述。
- 此外,在本单元的学习中,我们也了解了一些工具链的使用
(虽然每个工具的配置都不是什么善茬儿)。这些工具一方面可以辅助我们对代码的正确性进行验证,另一方面也为我们提供了测试的思路,在实用工具的同时人工生成针对性更浅的测试样例,同样也可以帮助我们更好的锁定bug。