301
社区成员
发帖
与我相关
我的任务
分享黑箱测试和白箱测试是软件测试中常用的两种测试方法,它们在测试的角度、方法和目的上有所不同。
黑箱测试是一种测试方法,测试人员不需要了解内部代码或系统结构,而是基于软件的需求规格说明和功能特性来设计测试用例,并验证软件的功能是否符合预期。黑盒测试关注的是软件的外部行为,旨在检查软件是否能够正确地处理输入数据并产生正确的输出。黑盒测试可以帮助发现功能性问题、性能问题、安全问题等。
白箱测试则是一种测试方法,测试人员需要了解软件的内部代码、结构和算法等信息,基于这些信息设计测试用例以揭示软件的内部错误和缺陷。白盒测试关注的是软件的内部逻辑、路径覆盖和代码覆盖等方面,旨在验证代码的正确性、健壮性和可维护性。白盒测试可以帮助发现代码逻辑错误、循环和条件覆盖不足等问题。
单元测试是针对软件中最小的可测试单位(如函数、方法)进行的测试。单元测试旨在验证各个单元的代码是否达到预期的功能要求,帮助开发人员在编写代码时及时发现和修复错误。
功能测试是以软件功能和需求为基础,测试软件是否按照用户的需求规格说明书执行,确保软件的功能符合预期并且达到要求的质量标准。
集成测试是将已通过单元测试的模块结合在一起进行测试,验证各个模块之间的交互与通信是否正常,确保系统各部分协同工作正常。
压力测试旨在评估系统在极端条件下的稳定性和性能表现,通过模拟大量用户访问或极大负载来测试系统的极限性能,并确定系统的承受能力和性能瓶颈。
回归测试是在对软件进行修改后,重新运行既有的测试用例,以确保修改后的代码没有破坏现有功能和引入新的问题。回归测试有助于在开发过程中保持软件质量的稳定性。
-2,147,483,648和2,147,483,647附近的值的数据,又如,在生成add_red_envelope_message指令时,应考虑tagSize为0的情况在本单元的架构中,我使用类似邻接链表的架构存储节点及其连接关系,即以下代码:
HashMap<Integer, HashMap<Integer, Integer>> links
在添加节点时,只需新增<节点,空HashMap>的映射关系,再添加/删除边时,只需在节点对应的HashMap新增/删除<对应节点,1>的映射关系,这种设计便于维护图的动态变化,也为深度/广度优先搜索提供了数据结构上的基础。
在维护图的连通性时,本次作业使用了并查集的思路。即在Network类中维护一个名为parent的HashMap用于存储节点的父节点,每次新增边时,调用union方法合并对应的两个节点。为提高性能,我采用了路径压缩和按秩合并的优化思路,即在搜索根节点时,通过循环更新的方法把父节点更新为根节点,在合并节点时,利用记录每个节点秩的HashMap,将秩较低的树合并至秩较高的树上。
public int findRoot(int id) {
int temId = id;
while (temId != parents.get(temId)) {
int oldParent = parents.get(temId);
parents.replace(temId, parents.get(oldParent));
temId = oldParent;
}
return temId;
}
public void union(int id1, int id2) {
int root1 = findRoot(id1);
int root2 = findRoot(id2);
if (root1 == root2) {
return;
}
if (ranks.get(root1) < ranks.get(root2)) {
parents.replace(root1, root2);
} else {
parents.replace(root2, root1);
if (ranks.get(root1).equals(ranks.get(root2))) {
int oldRank = ranks.get(root1);
ranks.replace(root1, oldRank, oldRank + 1);
}
}
}
需要注意的是,并查集不能解决删边的问题,而利用深度优先搜索重构并查集的时间复杂度为O(N),因此我采用了一种折中的方法,即删除边时,将delFlag置为true,当调用isCircle和queryBlockSum等需要检验节点连通性的方法时,若检测到delFlag为true,再重建并查集,并将delFlag置为false。这样的策略可以避免反复删除边但不检查联通性而导致的性能损失。
具体操作上,重建并查集时需要利用深度优先搜索算法,即从第一个节点开始,将从该节点开始可以搜索到的且未被访问的所有节点的父节点设置为该节点,然后对下一个未被访问的节点进行相同操作,直至所有节点都被访问,此时并查集重建完成。
public void rebuildGraph() {
HashMap<Integer, Integer> visited = new HashMap<>();
for (int i : links.keySet()) {
updateParent(i, visited, i);
}
dirtyFLag = false;
}
public void updateParent(int id, HashMap<Integer, Integer> visited, int root) {
if (!visited.containsKey(id)) {
parents.replace(id, root);
visited.put(id, 0);
for (int i : links.get(id).keySet()) {
updateParent(i, visited, root);
}
}
}
每次在{节点1,节点2}之间新增/删除边时,都需要从link中遍历节点1的所有邻接节点,若其与节点2相连,则将triangleCount加一/减一,这一操作的最坏时间复杂度为O(N),能满足性能要求(以下为新增边时的代码)。
for (int i : links.get(id1).keySet()) {
if (getPerson(i).isLinked(p2)) {
triangleCount++;
}
}
这里采用了比较简单的思路,当新增{节点1,节点2}之间的边时,均将对方与自己原有的bestAcquaintance比较并判断是否更新,当需要修改两节点关系的value或删除边时,则将changeFlag置为true。当需要获取bestAcquaintance时,若changeFlag为true则遍历该节点的acquaintance并计算出bestAcquaintance。
相较于这种思路,使用TreeMap的方法在修改时时间复杂度为O(logN),在查询时时间复杂度为O(1)。针对大多数测试样例,两种实现时间差距不大,但本次作业的实现更加适用于频繁修改但查询次数少的情况,而对于删除/修改value+查询的组合,受限于指令数目,性能并不会出现问题。
public void addNewRelation(Person person, int value1) {
int addedId = person.getId();
acquaintance.put(addedId, person);
value.put(addedId, value1);
if (!changeFlag) {
if (value1 > maxValue || value1 == maxValue && person.getId() < bestId) {
bestId = person.getId();
maxValue = value1;
}
}
}
public int getBestAcquaintance() {
if (changeFlag) {
int temMaxValue = -1;
for (int i : value.keySet()) {
if (value.get(i) > temMaxValue || value.get(i) == temMaxValue && i < bestId) {
bestId = i;
temMaxValue = value.get(i);
}
}
changeFlag = false;
maxValue = value.get(bestId);
//重新确定最小
}
return bestId;
}
每个tag的valueSum均应当从两个方面进行维护:
这种思路将时间复杂度分摊到了每次新增节点与修改节点间关系的操作中,每次操作的时间复杂度为O(N),避免了使用二重循环导致的性能问题(以下为添加节点时的代码)
public void addPerson(Person person) {
for (int i : persons.keySet()) {
if (person.isLinked(persons.get(i))) {
valueSum += 2 * person.queryValue(persons.get(i));
}
}
persons.put(person.getId(), person);
ageSum += person.getAge();
size++;
}
在上一段代码中可以注意到,tag内部维护了ageSum变量,可以随时利用ageSum/size计算均值。而在计算方差时,由于取整操作,必须代入原公式进行计算,其时间复杂度为O(N)。
由于节点间距离均为1,只需采用广度优先搜索即可。具体思路是从起点开始搜索,每访问到一个节点时,将其未被访问的邻接节点的距离设置为当前节点的距离加一,当访问到终点时搜索结束,其时间复杂度为O(N)。
public void bfsSearch(int searched, int target
, HashMap<Integer, Integer> visited, Queue<Integer> queue) {
visited.put(searched, 0);
boolean flag = false;
if (searched == target) {
return;
}
queue.add(searched);
while (!queue.isEmpty() && !flag) {
int needToSearch = queue.poll();
int basicLength = visited.get(needToSearch);
for (int i : links.get(needToSearch).keySet()) {
if (!visited.containsKey(i)) {
visited.put(i, basicLength + 1);
if (i == target) {
flag = true;
break;
}
queue.add(i);
}
}
}
}
本次作业全程未出现性能问题,主要原因在于,在设计架构时我就对每个方法的时间复杂度进行了分析,不允许任何方法出现大于等于O(N^2)的时间复杂度。同时,在测试时,我构造了高强度数据对性能进行极限测试。
这种策略是指将规格与具体实现分离,以实现更好的模块化、可维护性和可扩展性。其中规格描述了软件的功能需求、性能需求、接口规范等信息,而在具体实现时,只需要保证实现满足规格的要求,不需要过多考虑需求的细节。
当软件的需求发生改变时,只需要修改规格,而不需要直接修改代码。这降低了修改代码的风险和成本。规格与实现相分离也使得代码更加模块化,便于后续的维护和扩展。
规格为测试人员提供了明确的测试依据,他们可以基于规格编写测试用例,验证实现的正确性。由于规格是独立于实现的,因此测试可以更早地开始,并与开发并行进行,从而提高开发效率。
在构造测试样例时,需要利用规格中的前置条件覆盖规格中的每种情况,即正常行为和异常行为均需要进行测试。在检验正确性时,需要对以下内容进行测试:
通过使用assert系列方法,我们可以检测JUnit测试数据输出与规格输出是否一致。
相较于前两个单元,本单元简单了许多,在JML规格方面也并未设置过大的难度。在实际编写代码时只需要对照规格逐个方法逐个分支实现即可,在测试时则需要对照规格构造测试数据,也没有特别大的难度。相反的,我觉得本单元更多的考查了算法相关的内容,尤其侧重图论方面的内容,这反而给我代码的编写带来了较大的阻碍。
通过三次作业,我理解了JML的重要性,它避免了自然语言的不确定性,能提升软件开发的效率与准确性。