301
社区成员
发帖
与我相关
我的任务
分享单元测试
MyNetwork类中queryTripleSum(), queryCoupleSum(), deleteColdEmoji()三个方法的单元测试功能测试
集成测试
MyNetwork, MyPerson, MyTag, MyMessage, MyRedEnvepoleMessage, MyNoticeMessage, MyEmojiMessage。每个类都拥有各自的方法,因此对每个类进行整体的测试就是一种集成测试,可以使用JUnit来实现对每个类的整体测试。压力测试
回归测试
设计数据生成器
首先把数据生成分为四个阶段
| 阶段 | 指令1 | 指令2 | 指令3 | 指令4 |
|---|---|---|---|---|
| 第一阶段:构建网络 | add person | add relation | \ | \ |
| 第二阶段:修改关系 | mofidy relation | \ | \ | \ |
| 第三阶段:标签相关 | add tag | delete tag | add person to tag | delete person from tag |
| 第四阶段:消息相关 | store emoji id | add message | send message | \ |
然后把query指令分为3类
查询一: 和图相关
| 指令 | 简写 |
|---|---|
| query_value | qv |
| query_circle | qci |
| query_block_sum | qbs |
| query_triple_sum | qts |
查询二: 和熟人相关
| 指令 | 简写 |
|---|---|
| query_tag_value_sum | qtvs |
| query_tag_age_var | qtav |
| query_best_acquaintance | qba |
| query_couple_sum | qcs |
| query_shortest_path | qsp |
查询三: 和发送信息相关
| 指令 | 简写 |
|---|---|
| query_social_value | qsv |
| query_received_messages | qrm |
| query_popularity | qp |
| delete_cold_emoji | dce |
| query_money | qm |
根据JML确定每种指令涉及到的异常行为,在数据生成的时候要触发每一条指令的每一种异常行为
以mr指令为例,生成mr指令的格式为mr id1 id2 value, 因此可能触发的错误有:
def mr():
choose = random.randint(1,5)
if choose==1:
mr_wrong_unexisted_id()
elif choose==2:
mr_wrong_unexisted_rel()
elif choose==3:
mr_wrong_same_id()
elif choose==4:
mr_set_value("add")
elif choose==5:
mr_set_value("delete")
并且在许多指令中,都会涉及到:
id_set = []作为全局数组,存储了所有人的id,定义get_existed_id()来随机获取其中的id:
def get_existed_id():
index = random.randint(0, len(id_set) - 1)
id = id_set[index]
return id
并且考虑到,有时我们需要获得不存在的id可能会在数据生成器所配置的参数的范围内,或者范围外,所以需要两种随机获取不存在的id的方法
def get_unexisted_id_overlimit():
cnt = 0
id = random.randint(id_limit_down, id_limit + 100)
while id in id_set:
id = random.randint(id_limit_down, id_limit + 100)
cnt += 1
if cnt == 2*id_limit:
sys.stderr.write("在id_limit内id已用尽\n")
exit()
return id
def get_unexisted_id_withinlimit():
cnt = 0
id = random.randint(id_limit_down, id_limit)
while id in id_set:
id = random.randint(id_limit_down, id_limit)
cnt += 1
if cnt == 3*id_limit:
sys.stderr.write("在id_limit内id已用尽\n")
exit()
return id
rel_set = []作为全局数组,存储了所有关系的键值对,并且为了保证唯一性,所有关系都按照{id_min:id_max}的键值对的形式存储在rel_set中
rel_value = {}用于存储每个关系的value值,形式是使用一个二元组和value组成的键值对(id_min,id_max):value来存储每个关系的value值
然后使用以下两个方法来随机获取存在的关系,或者不存在的关系:
def get_unexisted_rel():
id1 = get_existed_id()
id2 = get_existed_id()
while id1 == id2:
id2 = get_existed_id()
idmin = id1 if id1 < id2 else id2
idmax = id2 if idmin == id1 else id1
cnt = 0
while {idmin: idmax} in rel_set:
id1 = get_existed_id()
id2 = get_existed_id()
while id1 == id2:
id2 = get_existed_id()
idmin = id1 if id1 < id2 else id2
idmax = id2 if idmin == id1 else id1
cnt += 1
if cnt == 3 * len(id_set):
sys.stderr.write("无法再添加新的边\n")
exit()
return {idmin:idmax}
def get_existed_rel():
index = random.randint(0, len(rel_set) - 1)
return rel_set[index]
其他指令的生成类似,这里不再赘述
根据一开始设计的四个阶段,来生成指令,并且把各种查询指令穿插在其中,以保证数据的复杂性
以add person to tag为例,根据配置的参数,输出一定数量的att指令。
然后再获取一个随机数,根据随机数的值来选择是否在att指令之后执行一次查询指令,以及执行那种查询指令
并对不同的指令赋予权重,qtvs和qtav指令对程序的算法效率要求更高,更耗时,为了测试程序的效率,给这两个查询指令更高的权重,以有目的生成更多的qtvs和qtav指令
for _ in range(times["att_times"]):
att()
# qtvs qtav -> 3
# qc qci qba qsp -> 2
# qbs qts qcs -> 1
for _ in range(random_query_time3):
possible = random.randint(-10, 11)
if possible == -5 or possible == -4 or possible == -3:
query_times["qtvs_times"] -= 1
qtvs()
elif possible == -2 or possible == -1 or possible == 0:
query_times["qtav_times"] -= 1
qtav()
elif possible == 1 or possible == 2:
query_times["qv_times"] -= 1
qv()
elif possible == 3 or possible == 4:
query_times["qci_times"] -= 1
qci()
elif possible == 5 or possible == 6:
query_times["qba_times"] -= 1
qba()
elif possible == 7 or possible == 8:
query_times["qsp_times"] -= 1
qsp()
elif possible == 9:
query_times["qbs_times"] -= 1
qbs()
elif possible == 10:
query_times["qts_times"] -= 1
qts()
elif possible == 11:
query_times["qcs_times"] -= 1
qcs()
在add message的阶段也是类似的设计,只不过测试涉及到更多查询指令,因此情况会多一些
for _ in range(times["am_all_times"]):
choose = get_choose(8)
if choose == 1:
am()
elif choose == 2:
arem()
elif choose == 3:
anm()
else:
aem()
# sm: evenly spaced
pos = random.randint(1,100)
if pos <= 8:
sm()
times["sm_times"] -= 1
# cn: evenly spaced
pos = random.randint(1, 100)
if pos <= 3:
cn()
times["cn_times"] -= 1
# sei: evenly spaced
possibility = 10 / times["am_all_times"]
pos = random.randint(1,100) / 100
if pos <= possibility:
sei()
# qsv qrm qp qm -> 10
# qtvs qtav -> 3
# qc qci qba qsp -> 2
# qbs qts qcs -> 1
for _ in range(random_query_time3):
possible = random.randint(-45, 11)
if -45 <= possible and possible <= -36:
query_times["qsv_times"] -= 1
qsv()
elif -35 <= possible and possible <= -26:
query_times["qrm_times"] -= 1
qrm()
elif -25 <= possible and possible <= -16:
query_times["qp_times"] -= 1
qp()
elif -15 <= possible and possible <= -6:
query_times["qm_times"] -= 1
qm()
elif possible == -5 or possible == -4 or possible == -3:
query_times["qtvs_times"] -= 1
qtvs()
elif possible == -2 or possible == -1 or possible == 0:
query_times["qtav_times"] -= 1
qtav()
elif possible == 1 or possible == 2:
query_times["qv_times"] -= 1
qv()
elif possible == 3 or possible == 4:
query_times["qci_times"] -= 1
qci()
elif possible == 5 or possible == 6:
query_times["qba_times"] -= 1
qba()
elif possible == 7 or possible == 8:
query_times["qsp_times"] -= 1
qsp()
elif possible == 9:
query_times["qbs_times"] -= 1
qbs()
elif possible == 10:
query_times["qts_times"] -= 1
qts()
elif possible == 11:
query_times["qcs_times"] -= 1
qcs()
配置参数,方便地控制测试数据的复杂度和条数
为了更好的进行压力测试,同时也照顾到互测3000条指令的限制,我设计了几个参数,来控制数据生成的复杂度和指令数量
同时为了更方便的看懂指令,这里对不同对象的id进行了范围划分
person的id范围在[1,100]
tag的id范围在[5000,5500]
message的id范围在[1000,2000]
# 1. id and tag range
id_limits = [1,10,20,100,10000,1000000,1000000000,2100000000]
id_limit_down = id_limits[0]
id_limit = id_limits[3]
tag_limits = [5000,5500]
tag_line = tag_limits[0]
tag_limit = tag_limits[1]
msg_limits = [100,200,500,1000]
msg_line = 1000
msg_limit = msg_line + msg_limits[3]
emj_limits = [10,20,100,1000]
emj_line = 101
emj_limit = emj_line + emj_limits[2]
然后指令每条指令的生成数量,来直接控制最后生成的测试数据的规模
# 2. operate times
times={
"ap_times":150,
"ar_times":500,
"mr_times":100,
"at_times":500,
"dt_times":100,
"att_times":2000,
"dft_times":200,
"am_all_times":4000,
"sm_times":800,
"cn_times":50,
"sei_times":100,
"dce_times":50
}
query_times_ori = {
"qv_times":5000,
"qci_times":5000,
"qtvs_times":5000,
"qtav_times":5000,
"qba_times":5000,
"qsp_times":5000,
"qbs_times":5000,
"qts_times":5000,
"qcs_times":5000,
"qsv_times":5000,
"qrm_times":5000,
"qp_times":5000,
"qm_times":5000
}
输出解析信息
由于直接生成的指令难以阅读,即使出现了bug也很难快速定位
因此在将生成的指令输出到标准错误的同时,相应的解析信息也会输出到标准错误,方便了解数据生成的真实情况
print("ap " + str(id) + " " + name + " " + str(age))
sys.stderr.write("add_person new:\tap " + str(id) + " " + name + " " + str(age) + "\n")
并且设置了不同指令,在不同异常下的计数器,以查看每种分支的覆盖情况
rel_counts = {"new":0, "wrong_rel_duplicated":0, "wrong_unexisted_id":0, "wrong_same_id":0}
qv_counts = {"query successful":0, "query wrong(unexisted_id)":0, "query wrong(unexisted_rel)":0}
sm_counts = { "success nom":0,
"success red":0,
"success not":0,
"success emj":0,
"wrong(unexisted_mid)":0}
cn_counts = {"success":0, "wrong(unexisted_pid)":0}
sei_counts = {"new":0, "wrong(dup_eid)":0}
完成后代码量在 2300+ 行,数据生成的效果如下:
标准输出stdin.txt
...略
qtvs 74 5174
qci 58 16
qtvs 145 5407
qtvs 78 5302
qci 49 40
qp 132
qp 296
qsv 49
aem 1545 200 0 880 881
qm 64
qp 157
qrm 33
qp 230
qtav 65 5179
qbs
qm 6
qci 41 8
qrm 85
qrm 32
...略
标准错误stderr.txt
...略
query_social_value success: qsv 65
query_popularity wrong(unexisted_eid): qp 286
query_money wrong(unexisted_eid): qm 137
query_popularity wrong(unexisted_eid): qp 131
#################################### stage eight: send message ####################################
every new: nom:106 red:114 notice:99 emoji:418
sm_counts: nom:82 red:69 notice:51 emj:200
remaining message: nom:24 red:45 notice:48 emj:218
{165: 13, 129: 0, 171: 0, 125: 0, 162: 0, 139: 0, 111: 0, 137: 0, 142: 0, 180: 0, 149: 0, 161: 0, 200: 0, 198: 0, 123: 0, 115: 0, 193: 0, 105: 0, 182: 0, 121: 0, 138: 0, 164: 0, 124: 0, 177: 0, 130: 0, 135: 0, 159: 0, 114: 0, 187: 0, 166: 0, 181: 0, 167: 0}
cn_counts: {'success': 56, 'wrong(unexisted_pid)': 52}
query_counts: qv_times: 8822, qci_times: 8959, qtvs_times: 11062, qtav_times: 11286, qba_times: 8705, qsp_times: 8738, qbs_times: 4606, qts_times: 4571, qcs_times: 4506, qsv_times: 15785, qrm_times: 15729, qp_times: 15575, qm_times: 15721
如何进行测试的
本测评机没有对输出进行正确性判断,而是通过多个同学的程序来进行输出的对拍,通过不同的输出来定位程序的问题
# generate date
os.system('python gene.py > stdin.txt 2> adjacent.txt')
# run jar
os.system(f'java -jar {jar_yrj} < stdin.txt > stdout_yrj.txt')
os.system(f'java -jar {jar_zyt} < stdin.txt > stdout_zyt.txt')
os.system(f'java -jar {jar_yyb} < stdin.txt > stdout_yyb.txt')
# judge
with open(file1,'r') as f1, open(file2,'r') as f2, open(file3,'r') as f3:
lines1 = f1.readlines()
lines2 = f2.readlines()
lines3 = f3.readlines()
cnt = 1
hasDiff = False
for line1, line2, line3 in zip(lines1, lines2, lines3):
if line1 != line2 or line1 != line3:
print("第一个不同的行" + str(cnt) + ":")
print("文件-yrj:", line1.strip())
print("文件-zyt:", line2.strip())
print("文件-yyb:", line3.strip())
hasDiff = True
break
cnt += 1
if hasDiff==False:
print("输出一样")
数据生成器有什么缺陷
MyNetwork中的属性private final HashMap<Integer,Person> persons = new HashMap<>();使用HashMap容器存储了所有的MyPerson对象,也就是图中的所有节点private final int[][] matrix = new int[3100][3100];作为领接矩阵,存储了整张图的信息,以备重建并查集的时候使用针对isCircle()方法,也就是判断两个节点是否处于同一个连通子图中,采用并查集的算法具有良好的时间复杂度
首先创建了Node类,用于表示并查集中存储的每个节点的信息,具有三个属性
private Node father;
private int rank;
private int id;
MyNetwork类中的属性private final HashMap<Integer, Node> nodes = new HashMap<>();存储了所有Node对象
然后创建了UniFind类,并且采用单例模式,来实现对并查集find()操作和merge()操作的封装
public class UniFind {
public static Node find(Node node) {
/* code */
}
/*@ requires node1 != node2 @*/
public static void merge(Node node1, Node node2) {
// 1. find father
/* code */
// 2. get rank
/* code */
// 3. merge
/* code */
}
}
并查集的更新和合并
addPerson()操作和isCircle()操作的时候,就会在并查集中加入新的节点,并进行更新addRelation()操作的时候,就会对并查集进行合并操作并查集的重构
modifyRelation()的删除操作的时候,由于此时情况较为复杂,所以直接舍弃之前的并查集;然后基于matrix来重构新的并查集Floyd和BFSmatrix,在查询两个节点之间最短路径的长度queryShortestPath()的时候,也需要依赖领接矩阵matrixFloyd算法for (int k = 0; k < V; k++) {
for (int i = 0; i < V; i++) {
for (int j = 0; j < V; j++) {
if (dist[i][k] != INF && dist[k][j] != INF && dist[i][k] + dist[k][j] < dist[i][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
}
}
}
}
add person以及add relation,modifyRelation之后都需要对matrix进行一遍Floyd算法,来更新最短路径的长度,而由于Floyd的时间复杂度过高,导致在第二次作业的强测中所有强测点都CTLE// 1. 通过matrix得到最短路的节点数
if (id1 == id2) {
return 0;
} else {
// 1. Array和HashSet
ArrayList<Integer> ids = new ArrayList<>();
HashSet<Integer> idsVisited = new HashSet<>();
// 2. get acquaintance
ids.addAll(((MyPerson) myNetwork.getPerson(id1)).getAcquaintanceIdList());
int cnt = 0;
int index = 0;
while (!ids.contains(id2)) {
// 3. add to HashSet
idsVisited.addAll(ids);
int idsSizeOld = ids.size();
for (;index < idsSizeOld; index++) {
MyPerson p = (MyPerson) myNetwork.getPerson(ids.get(index));
for (int j : p.getAcquaintanceIdList()) {
if (!idsVisited.contains(j)) {
ids.add(j);
}
}
}
// 4. cnt++;
cnt++;
}
return cnt;
}
queryValueSum()都对Tag中所有的Person进行双层遍历,时间复杂度为(O(n^2)),导致CTLEprivate int valueSum = 0;来存储valueSum的值,那在什么时候对这个值进行更新呢?valueSumvalueSumvalueSum,因此首先想到的方式是当涉及到关系的修改,就要作废所有的valueSum,然后重新计算MyNetwork使用private final HashMap<Integer, ArrayList<Tag>> personTags = new HashMap<>();来保存所有节点拥有这个节点的Tag的信息valueSum就可以if (personTags.containsKey(id1)) {
for (Tag tag1 : personTags.get(id1)) {
if (personTags.containsKey(id2)) {
if (personTags.get(id2).contains(tag1)) {
MyTag myTag = (MyTag) tag1;
myTag.addValueSum(2 * value);
}
}
}
}
ModifyRelation之后到需要重构并查集,导致时间复杂度过高而CTLE,因此需要改进这个方法MyNetwork的tripleSum,也就是三元环的数量减少的话,就说明了此时所删除的关系是某个三元环三条边中的一条,因此这个时候,这两个被删除关系的节点一定还在同一个连通子图中,因为他们有一个共同的第三个点,所以此时连通子图数量不变,也就不需要重构并查集tripleSum没有变化,才有可能新增了连通子图,这个时候再基于领接矩阵matrix来重构并查集final int oldTripleSum = tripleSum;
tripleSum -= p1.dupAcquaintance(p2);
if (tripleSum == oldTripleSum) {
/* code: rebuild unifind */
}
sendMessage()方法为例:/*@ public normal_behavior
@ requires containsMessage(id) && getMessage(id).getType() == 0 &&
@ getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()) &&
@ getMessage(id).getPerson1() != getMessage(id).getPerson2();
@ assignable persons[*], messages, emojiHeatList[*];
@ assignable getMessage(id).getPerson1().socialValue, getMessage(id).getPerson1().money;
@ assignable getMessage(id).getPerson2().messages, getMessage(id).getPerson2().socialValue, getMessage(id).getPerson2().money;
@ ensures !containsMessage(id);
@ ensures \old(getMessage(id)).getPerson1().getSocialValue() ==
@ \old(getMessage(id).getPerson1().getSocialValue()) + \old(getMessage(id)).getSocialValue() &&
@ \old(getMessage(id)).getPerson2().getSocialValue() ==
@ \old(getMessage(id).getPerson2().getSocialValue()) + \old(getMessage(id)).getSocialValue();
@ ensures (\old(getMessage(id)) instanceof RedEnvelopeMessage) ==>
@ (\old(getMessage(id)).getPerson1().getMoney() ==
@ \old(getMessage(id).getPerson1().getMoney()) - ((RedEnvelopeMessage)\old(getMessage(id))).getMoney() &&
@ \old(getMessage(id)).getPerson2().getMoney() ==
@ \old(getMessage(id).getPerson2().getMoney()) + ((RedEnvelopeMessage)\old(getMessage(id))).getMoney());
@ ensures (!(\old(getMessage(id)) instanceof RedEnvelopeMessage)) ==> (\not_assigned(persons[*].money));
@ ensures (\old(getMessage(id)) instanceof EmojiMessage) ==>
@ (\exists int i; 0 <= i && i < emojiIdList.length && emojiIdList[i] == ((EmojiMessage)\old(getMessage(id))).getEmojiId();
@ emojiHeatList[i] == \old(emojiHeatList[i]) + 1);
@ ensures (!(\old(getMessage(id)) instanceof EmojiMessage)) ==> \not_assigned(emojiHeatList);
@ ensures (\forall int i; 0 <= i && i < \old(getMessage(id).getPerson2().getMessages().size());
@ \old(getMessage(id)).getPerson2().getMessages().get(i+1) == \old(getMessage(id).getPerson2().getMessages().get(i)));
@ ensures \old(getMessage(id)).getPerson2().getMessages().get(0).equals(\old(getMessage(id)));
@ ensures \old(getMessage(id)).getPerson2().getMessages().size() == \old(getMessage(id).getPerson2().getMessages().size()) + 1;
type=0的Message,前置条件要求person1和person2要是熟人,并且不能是同一个人sendMessage(sampleId)方法执行后,网络中是否还含有这条MessageassertTrue(!myNetwork.containsMessage(sampleId))assertEquals(myNetwork.getPerson(personid1),
theOldSocialValue + message.getSocialValue());