本单元测试过程
黑箱测试与白箱测试
黑箱测试也称为功能测试,它关注于测试软件的功能和用户界面,而不需要了解内部的实现细节。测试者只关注于输入和输出,即输入一组数据或者执行一个操作,然后验证系统是否产生了期望的输出或行为。
这种测试方法更类似于最终用户的使用情况,因此能够更好地检测到系统的功能性问题。
白箱测试也称为结构测试或逻辑测试,它需要测试者具备对系统内部结构和代码的了解。测试者通过检查代码、路径覆盖、逻辑覆盖等手段来验证系统的正确性和健壮性。
这种测试方法通常由开发人员或具有编程经验的测试人员执行,因为它需要对系统的内部实现有深入的理解。
单元测试
单元测试是软件开发中验证最小代码单元(如函数、方法或类)是否按预期工作的自动化测试方法。它们是独立、自动化、重复、快速且隔离的,通过编写测试用例、运行测试、分析结果和迭代修复来确保代码质量和可靠性。
本单元作业中采用的Junit测试就是单元测试的一种类型。
功能测试
功能测试验证软件系统的功能是否符合规格要求和用户需求,重点在于用户界面、数据处理和业务逻辑是否正常工作,确保软件的功能性能够满足用户的期望。
压力测试
压力测试是一种用于评估系统在高负载情况下性能和稳定性的测试方法,通过模拟大量用户同时访问系统或其他高负载场景,评估系统的响应能力和承受能力。
回归测试
回归测试是一种用于确认软件修改后是否引入新错误或破坏已有功能的测试方法,通过重新运行之前的测试用例,以确保软件的质量和稳定性。
数据构造策略
一方面,利用评测机大量生成随机数据进行测试,尽可能地测试到更多的情况,提高测试覆盖率。
另一方面,根据方法的jml描述以及方法时间复杂度等信息手动构造测试数据,有针对性的测试方法的正确性以及性能。
图模型构建和维护策略
UML类图(以hw11为例)
本单元作业采用一个专门的工具类CircleTools管理图。在这个类中使用并查集维护图,并设置初始化、查找、合并、增加节点、删除节点等方法来维护图。
通过并查集,可以快速查询是否成环、BlockSum等问题,提高了性能。
除此之外,在MyNetWork类中设置一个整形变量triNum用于记录图中三角形数量,每次增加或删除关系时对其进行动态维护,在查询三角形数量时直接返回triNum的值即可。
性能问题
问题一
对于并查集,在删除关系时可以选择直接重新初始化,也可以选择使用dfs单独维护涉及到的两个节点。前者实现起来非常简单,但是会严重影响性能,所以采用后者,代码实现如下:
public void delLink(int id1, int id2) {
parent.put(id1, id1);
parent.put(id2, id2);
// 对id1执行DFS,重新连接所有与id1直接或间接连接的个体
dfsReset(id1);
// 对id2执行相同操作,但需要检查是否id2的根节点已经被设置为id1
if (find(id2) != id1) {
dfsReset(id2);
}
}
// 使用DFS重建并查集关系
private void dfsReset(int startId) {
Stack<Integer> stack = new Stack<>();
stack.push(startId);
Set<Integer> visited = new HashSet<>();
visited.add(startId);
while (!stack.isEmpty()) {
int current = stack.pop();
for (Person neighbor : persons.get(current).getAcq().values()) {
int neighborId = neighbor.getId();
if (!visited.contains(neighborId)) {
parent.put(neighborId, startId);
stack.push(neighborId);
visited.add(neighborId);
}
}
}
}
问题二
对于qcs指令,在查询coupleSum时,如果直接使用双重循环遍历查询会导致TLE。解决方法其实很简单,只进行一层循环(相当于计算了两编),然后将计算到的值除以2就是正确结果,代码实现如下:
public int queryCoupleSum() {
int count = 0;
for (MyPerson person : persons.values()) {
if (person.getAcq().size() == 0) {
continue;
}
int best = person.getBestValue();
if (persons.get(best).getBestValue() == person.getId()) {
count++;
}
}
return count / 2;
}
问题三
观察MyTag类中的getValueSum方法,如果对tag持有的person容器进行双重循环遍历则会导致超时(特殊情况:所有person全部加到一个tag里面)。解决方法也很简单,将内层循环由遍历tag持有的person容器改为遍历外层的person的熟人容器,如果熟人包含在tag里,则计算二者的value即可。这样修改后虽然还是双重循环,但是内层循环的数量会大大减少,进而大幅优化性能。代码实现如下:
public int getValueSum() {
int sum = 0;
//for (Person person1 : persons.values()) {
// for (Person person2 : persons.values()) {
// if (person1.isLinked(person2)) {
// sum += person1.queryValue(person2);
// }
// }
//}
for (MyPerson person : persons.values()) {
for (Person person1 : person.getLinkedPersons().values()) {
if (persons.containsKey(person1.getId())) {
sum += person.queryValue(person1);
}
}
}
return sum;
}
Junit测试
测试主要采取了两种方法。
第一种方法是根据需要测试的方法的jml描述,依据特殊情况手动构造测试数据。这种方法比较常规,对于方法本身理解越透彻就越能测试到更多情况。
另一方面,可以采用随机生成的数据进行测试。以第一单元为例,利用随机数生成一个具有大量节点的图,这个图中的person与relation都是随机的,通过这样的随机图尽可能地覆盖手动构造的数据没有覆盖到的情况。代码实现如下:
private static MyNetwork generateRandomNetwork(int size, int maxRelationStrength) {
Random random = new Random();
MyNetwork net = new MyNetwork();
// 随机添加人员
for (int i = 1; i <= size; i++) {
try {
shadowNet.addPerson(new MyPerson(i, "Person" + i, random.nextInt(100)));
net.addPerson(new MyPerson(i, "Person" + i, random.nextInt(100)));
} catch (EqualPersonIdException e) {
e.printStackTrace();
}
}
// 设置一个较低的概率阈值来生成较少的关系,比如 20%
double relationProbability = 0.2;
// 随机添加关系
for (int i = 1; i <= size; i++) {
for (int j = i + 1; j <= size; j++) {
if (random.nextDouble() < relationProbability) { // 使用设定的概率来决定是否创建关系
try {
shadowNet.addRelation(i, j, random.nextInt(maxRelationStrength) + 1);
net.addRelation(i, j, random.nextInt(maxRelationStrength) + 1);
} catch (PersonIdNotFoundException | EqualRelationException e) {
e.printStackTrace();
}
}
}
}
return net;
}
本单元学习体会
本单元作业在功能实现上相比前两单元较为简单,只需要根据jml填空就好了,需要注意的点基本就是性能问题。期待下一单元的作业。