回到首页

输入关键词,多个关键词用空格分隔。

← 工程原理与设计 · 系列目录

重新认识软件测试:从二度人脉设计测试用例

约 3 分钟更新于 2026-11-06
本页目录

2013 年和朋友做项目时,我们需要测试一个“二度人脉”功能。我先画了一张有 16 个用户的关系图,逐个列出每个人能够查到的联系人,随后发现很多结果相似,便想减少测试。

原稿有一个重要问题:减少测试起点,与删除关系图中的节点,是两件不同的事。删掉节点会改变路径和预期结果,不能因为一些结果看起来重复,就认为缩小后的图与原图等价。这次迁移保留这个案例,重新整理测试设计。

先说清楚“二度”

“朋友的朋友”至少有两种解释:恰好走两条边能到达的人,或最短距离不超过两条边的人。是否包括直接朋友、是否排除自己,也会影响答案。

这里采用明确的示例规则:关系是无向的;返回与起点最短距离为 1 或 2 的全部用户;不返回自己;同一用户只返回一次。生产系统如果采用单向通讯录关系,就必须另写规则与预期,不能直接沿用这个模型。

电话号码只是属性,不影响这里的图遍历逻辑,所以测试使用 A、B、C 等稳定标识。先检查成员集合,若产品还要求特定排序,再单独检查顺序。

用小图覆盖不同错误

表中 A-B 表示一条无向边。未出现在边中的起点仍是图中的独立节点。

场景输入边起点预期集合检查什么
孤立节点无A空集没有邻居时不返回自己
一条边A-BAB直接朋友被包括
长链A-B、B-C、C-DAB、C两跳停止,不带入 D
三角形A-B、B-C、C-AAB、C环路不带回自己
菱形A-B、A-C、B-D、C-DAB、C、D多条路径到同一用户只返回一次
不连通图A-B、C-DAB不带入另一个连通分量

对于重复关系、自环、未知用户,还应明确输入协议:拒绝、清洗还是允许。本文代码用集合存储邻居,重复边自然合并;自环不会使自己进入结果;查询不存在的用户则抛出异常。这些只是本例的约定。

保留一个容易检查的参考实现

优化后的实现可能有缓存、索引或复杂的数据访问。可以用一个简单的广度优先搜索作为小规模测试的参考,比较完整结果集合:

from collections import deque

def within_two_hops(graph, start):
    if start not in graph:
        raise KeyError(start)
    distance = {start: 0}
    queue = deque([start])
    while queue:
        current = queue.popleft()
        if distance[current] == 2:
            continue
        for neighbor in graph[current]:
            if neighbor not in distance:
                distance[neighbor] = distance[current] + 1
                queue.append(neighbor)
    return set(distance) - {start}

graph = {
    "A": {"B", "C"},
    "B": {"A", "D"},
    "C": {"A", "D"},
    "D": {"B", "C"},
}
assert within_two_hops(graph, "A") == {"B", "C", "D"}

这里假定邻接表完整:每个邻居都存在于字典中,而且每条无向边都在两端记录。输入校验属于另一层测试,不能把缺失邻接表产生的异常当作业务结果。

怎样减少测试才有依据

如果保留原来的 16 人图,只挑部分用户作为查询起点,图本身不变。这是减少测试查询,需要说明选中的起点覆盖了哪些结构,而不是只看返回数量是否一样。两个起点都返回 5 个人,也可能经过完全不同的路径。

如果把大图换成几张小图,就是重新设计测试输入。小图应有独立的预期结果,分别针对环、深度限制、去重、孤立点等错误;它们不是原图的无损压缩。原始大图仍可以作为集成回归用例保留。

除了例子,还能检查一些普遍性质:结果不包含自己;改变邻接列表顺序不影响结果;重复添加同一条边不影响结果;一致地重命名节点只会相应地重命名答案。再用小规模随机图比较优化实现和参考实现,能发现手写用例之外的遗漏。

软件测试的目标不是把用例数量减到最少,而是让每个用例对应一个清楚的行为或风险,同时让失败容易解释。小而明确的输入,通常比一张很大却说不清覆盖了什么的图更容易维护。

相关文章