301
社区成员
发帖
与我相关
我的任务
分享免责声明:为便于理解,本文有具体代码出现,请自觉不要照搬使用。
在经过课程内容较长时间的编码、测试和调试训练之后,我对软件测试和调试有了一些新的理解,特总结如下。
我们在初学“测试”的工程化方法的部分时,往往会陷入一种错觉,以为软件测试就仅仅是将工程的测试方法在整个软件上实施一遍,就可以保证至少较大程度上软件没有错误。然而,在先导课程使用了这样的方法之后,我曾苦恼地发现写了大量的单元测试,但是仍然没有发现或是不能发现全部的错误,也即测试工作的效率并不高。
这样的测试其实陷入了一个理解上的误区。测试的工程化方法给我们提供了工具,但也仅仅是提供了可供我们选择的一套质量保障工具包。覆盖式地使用所有工具,或者进行完全覆盖式的测试,并不是最优的测试方案,因为测试希望达到某种效率要求,即用尽可能小的工作量覆盖大部分可能发生错误的位置。正如韦伯在《以学术为业》中说明的,“如果有人以为数学家只要坐在书桌前,把弄米尺、计算器等,就能得到有学术价值的成果,这是很幼稚的想法。”——同样的情形也出现在软件测试中。
对于一些测试方法中的所谓“覆盖”,例如因果图分析,其本身也仅是可供我们选择的工具之一:即在我们认为错误不是出在常规的、容易发现的逻辑错误,而是很可能处于边界情况时,可以选择的一种工具。这种“覆盖”,即便由测试自身的原则来说明,也是不可能保证对正确性的完全保证——“测试具有不可穷尽性”。如果我们对于所有,包括那些本来可以用简单的、符合直觉的代码审查方法就能发现的错误,都用这样的方法来实现,那么大部分测试工作都将成为无用的累赘。
其他基础内容见先导课程第三次讨论区“软件测试笔记”。
使用一个名为UnionFindSet的类实现了一个建立在图节点上的并查集,但由于封装的缘故,将其他一些动态规划的变量也封装在内(这里仅有一个tripleNum)。同理,虽然每个结点的类型名称为UfsEntry,但是他们同时也是图的结点。
并查集实际上完全可以实现为一个“多个无序集合的集合”的结构,也就是有多个无序集合(HashSet),每个代表一个最大连通图,这样便于理解和维护。实现为树结构,并且在搜索时才压缩路径,实际上是一种懒惰优化——每次加入的操作复杂度o(1),直到查询其归属(根节点)才进行路径压缩而不是每次加入就搜索并加入到父节点上;那些加入后从不被查询的节点永远不会被执行路径压缩,从而节省了这一部分的复杂度。
定义内部类UfsEntry,因为这一类与并查集结构高度绑定,外部不需要使用;定义为静态类是其仅为一个结点数据结构,不使用外部类的方法。
这个并查集附带的种种高度特定程序相关的信息使之成为一个在此程序之外没有复用价值的数据结构,其实完全可以不使用泛型指定Map的key类型,而直接进行硬编码;这里使用泛型就是一个反面示例。
// MyNetwork.java:
public class MyNetwork implements Network {
private final UnionFindSet<Integer> unionFindSet = new UnionFindSet<>();
/* ... */
}
// UnionFindSet.java:
public class UnionFindSet<E> {
private static class UfsEntry {
private int groupSize;
private UfsEntry father;
private final HashSet<UfsEntry> graphLinks;
UfsEntry(UfsEntry father) {
this.groupSize = 1;
this.father = father;
graphLinks = new HashSet<>();
}
}
private final HashSet<UfsEntry> roots = new HashSet<>(); // Representative roots
private final HashMap<E, UfsEntry> entries = new HashMap<>(); // E elm -> UfsEntry
private int tripleNum = 0;
public void addRoot(E element) {
/* ... */
}
public void addLink(E element1, E element2) {
/* ... */
}
public void disLink(E element1, E element2) {
/* ... */
}
// Wrapper for recursive function pathExist().
private boolean pathExist(UfsEntry ufsEntry1, UfsEntry ufsEntry2) {
return pathExist(ufsEntry1, ufsEntry2, new HashSet<>());
}
private boolean pathExist(UfsEntry curEntry, UfsEntry tarEntry, HashSet<UfsEntry> checked) {
/* ... */
}
public int shortestPathLength(E id1, E id2) {
/* ... */
}
public boolean sameGroup(E element1, E element2) {
return findRoot(entries.get(element1)) == findRoot(entries.get(element2));
}
public int numOfGroup() {
return roots.size();
}
public int getTripleNum() {
return tripleNum;
}
// Wrapper for recursive function rerootGroup().
private int rerootGroup(UfsEntry startingEntry) {
return rerootGroup(startingEntry, startingEntry, 0, new HashSet<>());
}
private int rerootGroup(UfsEntry curEntry, UfsEntry startingEntry, int curTravelCount,
HashSet<UfsEntry> checked) {
/* ... */
}
private UfsEntry findRoot(UfsEntry ufsEntry) {
// Find the root.
UfsEntry trying = ufsEntry;
while (trying.father != null) {
trying = trying.father;
}
// Compress searching path.
if (ufsEntry.father != null) {
ufsEntry.father = trying;
}
return trying;
}
}
规格与实现是分离的,体现在两个方面:
对于比较多的方法都可使用简单动态规划进行优化。以bestAcquaintance为例:
public class MyPerson implements Person {
private Person bestAcquaintance = null;
private void rebuildBestAcquaintance() {
int maxValue = -1;
if (values.isEmpty()) {
bestAcquaintance = null;
return;
}
for (Person p: values.keySet()) {
int value = values.get(p);
if (
value > maxValue ||
(value == maxValue && p.getId() < bestAcquaintance.getId())
) {
bestAcquaintance = p;
maxValue = value;
}
}
}
public void addLink(Person person, int value) {
/* ... */
// Modify best acquaintance.
if (
bestAcquaintance == null ||
value > values.get(bestAcquaintance) ||
(value == values.get(bestAcquaintance) &&
person.getId() < bestAcquaintance.getId())
) {
bestAcquaintance = person;
}
}
public void modifyLink(Person person, int valueModAmount) {
/* ... */
// Modify best acquaintance.
assert bestAcquaintance != null;
if (person == bestAcquaintance) {
rebuildBestAcquaintance();
} else {
final int value = values.get(person);
if (
value > values.get(bestAcquaintance) ||
(value == values.get(bestAcquaintance) &&
person.getId() < bestAcquaintance.getId())
) {
bestAcquaintance = person;
}
}
}
public void deleteLink(Person person) {
/* ... */
// Modify best acquaintance.
if (person == bestAcquaintance) {
rebuildBestAcquaintance();
}
}
/* ... */
}
阅读规格时,我们不需要再进行证明,应该快速捕捉语义成分,结合对方法语义的初步理解而快速划分区域,在此基础上具体理解每一部分规格的语义。不要被逻辑和证明的细节阻碍了对语义的理解。以deleteColdEmojis为例,其规格为:
// Emojis.
// 1. All emojis with heat(org) >= limit still exist.
ensures (\forall int i; 0 <= i && i < \old(emojiIdList.length);
(\old(emojiHeatList[i] >= limit) ==>
(\exists int j; 0 <= j && j < emojiIdList.length; emojiIdList[j] == \old(emojiIdList[i]))));
// 2. old --deletion--> cur, no new emj added.
ensures (\forall int i; 0 <= i && i < emojiIdList.length;
(\exists int j; 0 <= j && j < \old(emojiIdList.length);
emojiIdList[i] == \old(emojjiIdList[j]) && emojiHeatList[i] == \old(emojiHeatList[j])));
// 3. length = num of emojis with heat(org) >= limit
ensures emojiIdList.length ==
(\num_of int i; 0 <= i && i < \old(emojiIdList.length); \old(emojiHeatList[i] >= limit));
ensures emojiIdList.length == emojiHeatList.length;
// Messages.
// 1. Still exist and not assigned to: not Emoji / Emoji but containsEmojiId(now).
ensures (\forall int i; 0 <= i && i < \old(messages.length);
(\old(messages[i]) instanceof EmojiMessage &&
containsEmojiId(\old(((EmojiMessage)messages[i]).getEmojiId())) ==> \not_assigned(\old(messages[i])) &&
(\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i])))));
ensures (\forall int i; 0 <= i && i < \old(messages.length);
(!(\old(messages[i]) instanceof EmojiMessage) ==> \not_assigned(\old(messages[i])) &&
(\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i])))));
// 2. Length.
ensures messages.length == (\num_of int i; 0 <= i && i < \old(messages.length);
(\old(messages[i]) instanceof EmojiMessage) ==>
(containsEmojiId(\old(((EmojiMessage)messages[i]).getEmojiId()))));
根据分析得到的语义,将验证方法分为执行前处理,执行中,返回值检验和执行后,按规格并注意代码风格(良好体现语义的命名)实现即可。
@Test
public void deleteColdEmoji_ok() {
// Emj 1.
HashSet<Integer> emojiIdsBiggerthanLimit = new HashSet<>();
for (int i = 0; i < network.getEmojiHeatList().length; i++) {
if (network.getEmojiHeatList()[i] >= limit) {
emojiIdsBiggerthanLimit.add(network.getEmojiIdList()[i]);
}
}
// Emj 2.
int[] oldEmojiIdList = network.getEmojiIdList();
// Emj 3.
int newEmojiIdListLength = emojiIdsBiggerthanLimit.size();
// Msg 1.
HashSet<Message> notAssignedMsgs = new HashSet<>(Arrays.asList(network.getMessages()));
// Execute
final int ret = network.deleteColdEmoji(limit);
// Return value.
Assert.assertEquals(network.getEmojiIdList().length, ret);
// Emj 1.
for (int emojiId: network.getEmojiIdList()) {
emojiIdsBiggerthanLimit.remove(emojiId);
}
Assert.assertTrue(emojiIdsBiggerthanLimit.isEmpty());
// Emj 2.
HashSet<Integer> newEmojiIdList = new HashSet<>();
for (int now: network.getEmojiIdList()) { newEmojiIdList.add(now); }
for (int prev: oldEmojiIdList) { newEmojiIdList.remove(prev); }
Assert.assertTrue(newEmojiIdList.isEmpty());
// Emj 3.
Assert.assertEquals(newEmojiIdListLength, network.getEmojiIdList().length);
// Emj 4.
Assert.assertEquals(network.getEmojiIdList().length, network.getEmojiHeatList().length);
// Msg 1.
notAssignedMsgs.removeIf(msg->
(msg instanceof EmojiMessage)
&& !network.containsEmojiId(((EmojiMessage) msg).getEmojiId()));
HashSet<Message> newMsgs = new HashSet<>(Arrays.asList(network.getMessages()));
Assert.assertTrue(newMsgs.containsAll(notAssignedMsgs));
notAssignedMsgs.forEach(msg -> Assert.assertTrue(msgStrictEquals(msg, network.getMessage(msg.getId()))));
// Msg 2.
Assert.assertEquals(notAssignedMsgs.size(), newMsgs.size());
}
如上文所述,应该把较容易触发这一方法发生错误的数据情况考虑到数据生成中,因此对数据生成进行部分修改。