BUAA-OO-Unit3 总结
一、测试过程总结
黑箱测试与白箱测试的理解
- 黑箱测试:其核心思想是将被测代码视为一个黑盒子,我们不关心其中的结构和实现细节,直接根据代码的需求和规格说明对其进行测试即可。黑盒测试的重点在于检查代码是否满足功能需求,这种方法的优点是与代码的具体实现分离,即使代码内部结构发生变化,只要外部行为不变,测试用例依然有效。
- 白箱测试:白箱测试是一种基于代码的测试方法,与黑箱测试相反,白箱测试需要深入代码的内部结构和具体实现。测试者需要查看并理解源代码,设计测试用例来覆盖尽可能多的代码路径、分支、循环和条件,以确保代码的每一部分都经过了验证。它能帮助我们理解代码的执行流程,确保代码逻辑的正确实施。
单元测试、功能测试、集成测试、压力测试、回归测试的理解
- 单元测试:单元测试是对测试代码的最小可测试单元,通常对是一个函数、一个类或一个模块的测试。
- 功能测试:功能测试是为保障代码功能满足给定需求的测试,测试时不会考虑软件的内部结构,只关注最终的功能。
- 集成测试:集成测试是在单元测试之后进行的,用于检查不同模块之间的接口是否能够正确协同工作。它关注的是模块间的数据传递、控制流以及相互依赖关系,目的是发现接口错误、数据一致性问题等。
- 压力测试:压力测试是为了评估系统在极端条件下的性能和稳定性。它通过模拟大量并发用户访问、大数据量处理或资源限制情况,来检测系统的最大处理能力、响应时间、资源利用情况以及系统何时会达到崩溃点。
- 回归测试:回归测试是在代码修改或新功能添加后进行的一种测试,目的是确保现有的功能没有因为最近的更改而受到影响。它通常会重新执行之前成功运行的测试用例,以验证软件的已有功能仍然能够正常工作。
数据构造策略
本单元的测试的数据构造我主要遵循了随机原则、全面原则以及多次原则,下面我将对我的数据构造策略进行简单介绍。
随机原则:本单元数据构造中person的id信息、不同person之间的关系信息等均遵循随机原则构造,以确保数据的随机性。
具体实现如下:
private void addPerson(int id) {
MyPerson p = new MyPerson(id, "1", 1);
persons.put(id, p);
try {
myNetwork.addPerson(p);
} catch (EqualPersonIdException e) {
throw new RuntimeException(e);
}
}
private void addRelation(int id1, int id2) {
Random random = new Random();
try {
int n = random.nextInt();
while (n >= 0) {
n = random.nextInt();
}
myNetwork.addRelation(id1, id2, n);
} catch (PersonIdNotFoundException | EqualRelationException e) {
throw new RuntimeException(e);
}
}
全面原则:本单元测试数据测试从零图到完全图全部进行了测试,以满足数据全面性。
具体实现如下:
private void modifyRelation(int id1, int id2) {
Random random = new Random();
try {
int n = random.nextInt();
while (n >= 0) {
n = random.nextInt();
}
myNetwork.modifyRelation(id1, id2, n);
} catch (PersonIdNotFoundException | RelationNotFoundException | EqualPersonIdException e) {
throw new RuntimeException(e);
}
}
for (int i = 0; i < n; i++) {
addPerson(i);
idSet.add(i);
}
int times = n*(n-1)/2;
for (int i = 0; i < times; i++) {
createRelation();
test();
}
多次原则:本单元测试均进行了多次测试,以确保测试的多样性。
具体实现如下:
public void testQueryCoupleSum() {
int n = 30;
for (int i = 0; i < n; i++) {
addPerson(i);
idSet.add(i);
}
int times = n*(n-1)/2;
for (int i = 0; i < times; i++) {
createRelation();
test();
}
for (int i = 0; i < times; i++) {
delRelation();
test();
}
}
二、架构设计:图模型构建和维护策略
依据JML的规格描述,我们需要实现一个人际关系网络:MyNetwork,通过分析可以确定使用图的数据结构实现。为了实现更好地查询性能,采用HashMap存储数据。此外,我们还需要根据具体函数JML规格,依据图算法实现特定函数的功能,下面我将对本单元作业的图模型构建和维护策略进行介绍。
并查集与连通块
hw9中需要我们实现isCircle()和queryBlockSum()函数,根据其JML规格可以发现我们需要通过函数判断图中两个点之间是否连通,以及图中连通块的数量。
据此,我设计了一个MyBlock类作为连通块存储在同一连通块中的person,Myblock有一个特定属性blockId,根据blockId是否相同我们可以判断两个person是否在同一连通块中,而在MyNetwork中有一个HashMap存储MyBlock。当执行addRelation时,两个person的连通块使用mergeBlcok进行融合。当使用modifyRelation时,使用dfs重建图中连通块。
public class MyBlock {
private int id;
private HashMap<Integer, Person> persons;
public MyBlock(int id) {
this.id = id;
persons = new HashMap<>();
}
public int getId() {
return id;
}
public HashMap<Integer, Person> getPersons() {
return persons;
}
public void addPerson(Person person) {
...
}
public void mergeBlock(MyBlock myBlock) {
...
}
public boolean isExist(Person person) {
return persons.containsKey(person.getId());
}
}
查询最短路径
使用优先队列优化的Dijkstra算法查询。
具体实现:
private int shortest(int id1, int id2) {
MyPerson p1 = (MyPerson) getPerson(id1);
MyPerson p2 = (MyPerson) getPerson(id2);
HashMap<Person, Integer> distance = new HashMap<>();
HashMap<Integer, Boolean> visited = new HashMap<>();
for (Person p : persons.values()) {
distance.put(p, Integer.MAX_VALUE);
visited.put(p.getId(), false);
}
distance.put(p1, -1);
PriorityQueue<Person> pq =
new PriorityQueue<>(Comparator.comparingInt(distance::get));
pq.add(p1);
while (!pq.isEmpty()) {
MyPerson current = (MyPerson) pq.poll();
if (current.equals(p2)) {
return distance.get(p2);
}
if (visited.get(current.getId())) {
continue;
}
visited.put(current.getId(), true);
for (Person neighbor : current.getAcquaintance().values()) {
int newDistance = distance.get(current) + 1;
if (!visited.get(neighbor.getId()) && newDistance < distance.get(neighbor)) {
distance.put(neighbor, newDistance);
pq.add(neighbor);
}
}
}
return distance.get(p2);
}
维护策略
- 脏位维护:连通块在mr后会改变,需要重置,如果mr就重建会极大影响性能,所以设置一个dirty位用于判断是否使用了mr,再在查询时进行判断,如果dirty为1就重建,再输出结果。
- 动态维护: queryTagValueSum函数的实现可以在MyTag中设置valueSum,对其进行维护,以防止每次查询就重新计算。
三、性能问题及其修复
对valueSum进行动态维护,防止多次qtvs指令导致的ctle。
四、规格与实现分离
在完成本次作业的时候会发现,如果按照JML规格实现某些函数,可能会出现O(n^2)等时间复杂度较高、性能交叉的算法,这会导致我们在强测中出现ctle的错误,因此我们不能使用JML中的实现方法。实际上,为了规格的清晰和准确,JML表述往往采用比较朴素的方法,因此我们在实现时,需要在理解规格的基础上,找到更高效的实现方法。即: 规格是指定需求,实现是完成指定需求的方法。 我们要做的就是找到完成指定需求的最高效的方法。
五、Junit测试总结
利用JML规格设计
逐级比对JML规格中的每个信息,确保测试能够覆盖JML中每一个变化,同时不变量也保持不变。
具体来说就是:
- 比对assignable对象
- 比对ensures内容
- 比对result内容
- 比对exception内容
Junit测试检验代码实现与规格的一致性的效果
在测试覆盖JML规格中每一种情况以及较强的测试数据下,Junit测试能够非常好的检验代码实现与规格的一致性的效果。
六、学习体会
面向JML规格编程,让我有一种面向黑箱编程的感觉。我只需要在理解JML规格及其指定需求后,用更加高效的方法实现它即可。比较其他编程形式,JML给了我们编程的方向及其可能的实现方法,在完成后还能够根据JML来编写更加完备的测试方案。相对而言,面向JML规格编程能够让我们的代码bug更少,更加严谨。