- 谈谈你对黑箱测试、白箱测试的理解
白盒测试也称为结构测试,主要用于检测软件编码过程中的错误。程序员的编程经验、对编程软件的掌握程度、工作状态等因素都会影响到编程质量,导致代码错误。
黑盒测试又称为功能测试,主要检测软件的每一个功能是否能够正常使用。在测试过程中,将程序看成不能打开的黑盒子,不考虑程序内部结构和特性的基础上通过程序接口进行测试,检查程序功能是否按照设计需求以及说明书的规定能够正常打开使用。
在目前阶段,我觉得最常用的还是黑盒测试,即面向功能的测试。这使得我们不用关系代码的具体实现,只需输入测试数据、运行代码、得到结果并进行对拍即可。课程组的中测和强测均是采用这种方式。然而白盒测试并非没有用武之地,在大型工程上,白盒测试能够深入软件过程性细节,仔细分析程序的结构。事实上,我们在写完代码时候,会从头到尾对代码进行阅读,并根据代码手搓一些简单数据进行针对测试,以检验其基本的正确性,这也体现了白盒测试的思想。
- 对单元测试、功能测试、集成测试、压力测试、回归测试的理解
单元测试、功能测试、集成测试、压力测试、回归测试是软件测试的五种主要类型,它们各有所长,互为补充。
-
单元测试是完成最小的软件设计单元(模块)的验证工作,目标是确保模块被正确的编码,使用过程设计描述作为指南,对重要的控制路径进行测试以发现模块内的错误,通常情况下是白盒测试,对代码风格和规则、程序设计和结构、业务逻辑等进行静态测试,及早发现解决不易显现的错误。
JUnit
测试是单元测试的典范。 -
集成测试是在软件系统集成过程中所进行的测试,其主要目的是检查软件单位之间的接口是否正确。
OO课程中很少用。 -
系统测试是在软件开发完成后进行的一种测试,其目的是检查整个系统是否符合用户需求和规格说明书中所列出的需求。这个也是查的资料,不太了解。
-
压力测试是一种通过对系统施加压力来检测系统性能的方法。这就是我们喜闻乐见的测试方式了。在实际测试的时候,我们不一定要严格按照数据限制来生成数据,可以更多考虑数据的极端情况,增加数据生成的密度。这样我们可以生成更多更复杂的数据,快速发现我们设计实现重点问题。
-
回归测试是在对软件进行修改后重新运行现有的测试用例以确保修改没有引入新错误或导致其他代码发生故障。Bug 修复环节采取了回归测试,只有通过了错误点且还能够通过原来通过的所有测试点,Bug 修复才算成功。通过回归测试,能够使软件质量系统性提升。
单元测试
- 是否使用了测试工具
采用了测试工具。第一次采用了 JUnit
测试,但是对每一个模块都要编写测试代码,制造相应的数据,这实在过于繁琐。模块数量是远远超过功能数量的,如果追求对模块的全覆盖,必然就会牺牲强度,损害测试的整体性,何况作业时间紧、任务重,测试数据难度大,于是在后面几次作业中,我都采用了黑箱测试,编写数据生成器与他人对拍,还用来一些大佬的评测机,进一步提高代码质量。
- 数据构造有何策略
随机生成的,按照题目的数据要求生成即可。但是,为了更好的寻找 bug,可能要对随机生成的参数做一些调整。印象很深的是 hw10
的互测中,有一位同学的 bug 复现率很低,研究数据生成器之后发现,是数据中人与人的关系不够稠密所致。在压缩了总的生成人数后,同学的 bug 就“原形毕露了”。
OKTest
的构造颇有难度。hw10
的 OKTest
数据生成器写了350行,hw11
的数据生成器写了100行,成功发现大量bug。OKTest
的数据生成要“反其道而行之”,凡是规格中规定不能有副作用的,要想方设法修改数据,产生副作用;凡是规格中约束的,要想方设法生成不符合约束的情况;方式规格中规定异常的,要想方设法改变异常的行为。
作业类图
hw9
qbs
采用并查集加路径压缩,qts
没进行维护,每次都重新计算,用了一个 的算法,不仅引入了 bug
还差点超时,坏了。
并查集用法:
主要就是 find
函数,在连通两棵树的同时,让两棵树的点都尽可能直接指向根节点。
private void mergeNode(int id1, int id2) {
nodePeople.put(findRoot(id1), findRoot(id2)); //find id2 root
}
private int findRoot(int id) {
if (nodePeople.get(id) == id) {
return id;
}
int father = findRoot(nodePeople.get(id));
nodePeople.put(id, father);
return father; //path compression
}
qts算法
public int queryTripleSum() { //Triple sum
if (!relaAltered) {
return queryTripleSum;
}
int cnt = 0;
HashMap<Integer, HashSet<Integer>> tmpMap = new HashMap<>();
for (int id : people.keySet()) {
tmpMap.put(id, new HashSet<>());
} //建图
for (int id : people.keySet()) { //将无向图转化为有向图
MyPerson myPerson = (MyPerson) people.get(id);
for (int id1 : myPerson.getAcquaintance().keySet()) {
MyPerson myPerson1 = (MyPerson) people.get(id1);
if (myPerson.getAcquaintance().size() < myPerson1.getAcquaintance().size()
|| (myPerson.getAcquaintance().size() == myPerson1.getAcquaintance().size()
&& myPerson.getId() < myPerson1.getId())) {
tmpMap.get(id).add(id1); //if equals added twice 这个地方出问题了,之前有可能重复添加边
} else {
tmpMap.get(id1).add(id);
}
}
}
for (int id : tmpMap.keySet()) { //枚举符合条件的三角形
for (int id1 : tmpMap.get(id)) {
for (int id2 : tmpMap.get(id1)) {
if (tmpMap.get(id).contains(id2)) {
cnt++;
}
}
}
}
queryTripleSum = cnt;
relaAltered = false; //updated
return queryTripleSum;
}
考虑给所有的边一个方向。具体的,如果一条边两个端点的度数不一样,则由度数较小的点连向度数较大的点,否则由编号较小的点连向编号较大的点。不难发现这样的图是有向无环的。注意到原图中的三元环一定与对应有向图中所有形如 的子图一一对应,这是由弱序关系所保证的。只需要枚举 u
的出边,再枚举 v
的出边,然后检查 w
是不是 u
指向的点即可。
使用 JUnit
测试,漏洞百出,所幸强测没有测出来。
hw10
还是并查集,涉及删改就重建(摆烂),所幸并查集的时间复杂度并不高。采用路径压缩之后的时间复杂度也仅仅比 O(n)
大一点。qts
改成了动态维护。动态维护虽然朴素,但是很实用,性能也非常不错。
重点说下 qts
动态维护。其巧妙之处在于添加关系前或者删除关系(边)e:<u, w>
后枚举一侧顶点 u
的相邻顶点 v
,如果 v
和原来的边 w
相连,则qts计数对应增加或减少,十分巧妙。
//删除边
myPerson1.removeAcquaintance(myPerson2);
myPerson2.removeAcquaintance(myPerson1);
for (int id : myPerson1.getAcquaintance().keySet()) {
if (myPerson2.getAcquaintance().get(id) != null) { queryTripleSum--; }
}
//增加边
for (int id : myPerson1.getAcquaintance().keySet()) {
if (myPerson2.getAcquaintance().get(id) != null) { queryTripleSum++; }
}
myPerson1.addAcquaintance(myPerson2);
myPerson2.addAcquaintance(myPerson1);
使用黑箱测试,编写了数据生成器,但是 OKTest
还是锅了。
hw11
结合堆优化的 Dijkstra
算法和并查集,使用了学长推荐的一个算法。qlm
写得相当痛苦。当时还二阳了,不过也学到了不少知识。算法的思路为先通过一次 dijkstra
算法求取最短路径树,再根据两种情况,枚举边,计算可能的最小环,得到答案。
public int queryLeastMoments(int id) throws PersonIdNotFoundException, PathNotFoundException {
MyPerson origin = (MyPerson) people.get(id);
if (origin == null) { throw new MyPersonIdNotFoundException(id); }
HashSet<Integer> visited = new HashSet<>();
HashMap<Integer, Integer> dis = new HashMap<>();
HashMap<Integer, Integer> tree = new HashMap<>(); //构建最短路路径森林
for (int id1 : people.keySet()) {
tree.put(id1, id1);
dis.put(id1, 0x3f3f3f3f);
}
PriorityQueue<MyPair<Integer, Integer>> queue = new PriorityQueue<>();
dis.put(id, 0);
queue.add(new MyPair<>(0, id));
while (queue.size() != 0) {
int x = queue.poll().getValue();
if (visited.contains(x)) { continue; }
visited.add(x);
MyPerson myPerson = (MyPerson) people.get(x);
for (Map.Entry<Integer, Person> personEntry : myPerson.getAcquaintance().entrySet()) {
Integer oldDisY = dis.get(personEntry.getKey());
Integer oldDisX = dis.get(x);
if (oldDisY > oldDisX + myPerson.queryValue(personEntry.getValue())) {
if (x != id) { tree.put(personEntry.getKey(), x); } //来自同一颗最短路径树,并查集维护
oldDisY = oldDisX + myPerson.queryValue(personEntry.getValue());
dis.put(personEntry.getKey(), oldDisY);
queue.add(new MyPair<>(oldDisY, personEntry.getKey()));
}
}
}
int ans = 0x3f3f3f3f;
for (Map.Entry<Integer, Person> personEntry : origin.getAcquaintance().entrySet()) {
if (!personEntry.getKey().equals(tree.get(personEntry.getKey()))) { //首先判断这个点是否为树根,如果是的话,就不会形成环
ans = Math.min(ans,
dis.get(personEntry.getKey()) + origin.queryValue(personEntry.getValue()));
}
}
for (Map.Entry<Integer, Person> personEntry : people.entrySet()) {
MyPerson myPerson1 = (MyPerson) personEntry.getValue();
for (Map.Entry<Integer, Person> personEntry1 : myPerson1.getAcquaintance().entrySet()) {
if (myPerson1.getId() != id && personEntry1.getKey() != id
&& findRoot(myPerson1.getId(), tree) !=
findRoot(personEntry1.getKey(), tree)) { //两个点不能在一颗树上,否则无法形成环,这里顺带路径压缩降低时间复杂度
ans = Math.min(ans,
dis.get(myPerson1.getId()) + dis.get(personEntry1.getKey())
+ myPerson1.queryValue(personEntry1.getValue()));
}
}
}
if (ans == 0x3f3f3f3f) { throw new MyPathNotFoundException(id); }
return ans; //3 hours to finish
}
使用黑箱测试,顺利过关qwq。
一些其他的问题
规格的编写和实际的实现有较大差异,规格需要与具体的实现分离。根据规格编写程序时不一定要严格按照规格中的数据结构和方法实现,只要满足规格的基本约束即可。根据方法编写规格时,要注意抽象,不能拘泥于具体的数据结构和方法实现。但是对于有重大正确性需求的工程有很大的辅助作用。
OK
测试对于检验代码实现与规格的一致性功不可没,但是检测 OKTest
的正确性却太难了@_@。
心得
JML太难了,上机考试完全不会,都说这玩意抽象且不实用,我却觉得它和有趣,而且这是安全攸关领域保障安全的核心方法。以后我要继续研究它,享受数理逻辑和编程结合的乐趣。