中国象棋走马规则测试实战:用因果图法设计测试用例的5个关键步骤
中国象棋走马规则测试实战:用因果图法设计测试用例的5个关键步骤
作为一名在软件测试领域摸爬滚打了多年的工程师,我常常遇到一些逻辑复杂、条件交织的功能点,让人感觉测试用例设计起来无从下手。比如,一个看似简单的“中国象棋中马的走法”,背后就隐藏着棋盘边界、日字型移动、绊马腿、棋子碰撞等多重规则的组合。面对这种“牵一发而动全身”的输入输出关系,传统的等价类划分或边界值分析往往显得力不从心,容易遗漏那些由多个条件组合触发的隐蔽缺陷。
这时,因果图法就成了我们测试工具箱里的一把“瑞士军刀”。它不直接处理单个输入,而是专注于梳理原因(输入条件)与结果(输出动作)之间错综复杂的逻辑关系,尤其擅长处理那些包含约束条件(比如某些条件不能同时成立)的场景。今天,我就以“中国象棋走马”这个经典案例为蓝本,结合我自己的项目实战经验,为你拆解运用因果图法设计测试用例的五个核心步骤。无论你是刚入行的测试新手,还是想系统梳理黑盒测试方法的中级工程师,这篇文章都将带你走完一个从规则分析到用例落地的完整闭环。
1. 第一步:拆解规则,明确定义“因”与“果”
任何测试设计的第一步,都是深入理解需求。对于因果图法,这一步的目标是精准地识别出所有独立的“原因”和“结果”。这听起来简单,但实际操作中,模糊或错误的理解会导致后续全盘皆输。
原因,指的是所有可能影响系统行为的输入条件或前提。在象棋走马规则中,我们不能简单地把“马的走法”当成一个原因,而是要把它拆解成一系列原子级的、可被直接判定为“真”或“假”的输入条件。
结果,则是系统针对这些输入组合所做出的具体响应或输出动作。
让我们回到中国象棋的棋盘。马的走法规则,用自然语言描述通常如下:
- 如果落点在棋盘外,则不移动棋子。
- 如果落点与起点不构成“日”字型,则不移动棋子。
- 如果落点处有己方棋子,则不移动棋子。
- 如果落点方向的邻近交叉点有棋子(即“绊马腿”),则不移动棋子。
- 如果以上情况均不满足,且落点处无棋子,则移动棋子到该位置。
- 如果以上情况均不满足,且落点处为对方棋子(非老将),则移动棋子并吃掉对方棋子。
- 如果以上情况均不满足,且落点处为对方老将,则移动棋子,并提示战胜对方,游戏结束。
现在,我们需要将这些描述转化为测试语言。关键在于原子化。例如,“落点处有棋子”这个描述,需要进一步拆分为“落点处有己方棋子”和“落点处有对方棋子”两个独立的原因,因为它们的逻辑影响是不同的。
经过分析,我们可以梳理出以下原因和结果:
原因(输入条件):
- C1: 落点在棋盘内(与规则1相反,我们通常取“有效”条件作为原因)。
- C2: 落点与起点构成“日”字型。
- C3: 落点方向的邻近交叉点无棋子(即不“绊马腿”)。
- C4: 落点处无棋子。
- C5: 落点处为对方棋子(非老将)。
- C6: 落点处为对方老将。
注意:这里有一个重要的处理技巧。规则1-4描述的都是“不移动”的无效情况,而规则5-7描述的是有效的移动。在因果图分析中,我们通常更关注导致有效动作的原因组合。因此,我将“落点在棋盘内”作为原因,而“落点在棋盘外”则作为其取反(NOT)情况,在因果图中通过逻辑非门(~)表示。同样,“构成日字型”、“无绊马腿”等也是取的有效条件。
结果(输出动作):
- E1: 不移动棋子(对应规则1-4)。
- E2: 移动棋子到落点(对应规则5-7的基础动作)。
- E3: 除去对方棋子(对应规则6)。
- E4: 提示获胜,游戏结束(对应规则7)。
通过这一步,我们就把一段充满“如果...那么...”的自然语言规则,转化成了结构清晰、无歧义的测试输入输出列表,为下一步绘制逻辑关系图打下了坚实的基础。
2. 第二步:绘制因果图,可视化逻辑关系与约束
有了清晰的“因”和“果”,下一步就是用图形化的方式把它们之间的逻辑关系画出来。因果图使用四种基本的逻辑门来连接原因和结果:
- 恒等(Identity):若原因成立,则结果成立。
- 非(NOT):若原因成立,则结果不成立;反之亦然。
- 或(OR):若多个原因中至少一个成立,则结果成立。
- 与(AND):仅当所有原因都成立时,结果才成立。
此外,还需要识别原因与原因之间、结果与结果之间的约束关系。常见的约束有:
- 异约束(E, Exclusive):原因a和原因b不能同时为真(最多一个为真)。
- 或约束(I, Inclusive):原因a、b、c中至少有一个必须为真。
- 唯一约束(O, Only One):原因a和原因b有且仅有一个为真。
- 要求约束(R, Requires):若a为真,则b也必须为真。
回到我们的案例。首先,我们注意到关于落点处棋子状态的三个原因(C4无子, C5对方非老将, C6对方老将)是互斥的,一个位置不可能同时无子又有子,也不可能同时是对方非老将和对方老将。因此,需要在C4、C5、C6之间加上异约束(E)。
其次,结果E2(移动棋子)、E3(吃子)、E4(获胜)也是互斥的,一次移动只能产生一种最终状态。它们之间需要唯一约束(O)。
现在,我们来构建逻辑。导致“不移动棋子(E1)”的条件是清晰的,即规则1-4中任意一条成立。用逻辑门表示就是:
(NOT C1) OR (NOT C2) OR (NOT C3) OR C7(落点有己方棋子) 会导致 E1。
提示:为了简化,我们可以引入一个中间节点11,表示“移动的基本条件满足”,即
11 = C1 AND C2 AND C3 AND (NOT C7)。这样,E1 = NOT 11,逻辑更清晰。
而有效的移动(结果E2、E3、E4)都必须在中间节点11为真的前提下才会发生。然后,再根据落点处的具体情况(C4, C5, C6)来决定具体是哪个结果:
- 如果 11 为真 且 C4 为真(落点无子) -> 则 E2(移动)。
- 如果 11 为真 且 C5 为真(落点是对方非老将) -> 则 E2(移动)且 E3(吃子)。
- 如果 11 为真 且 C6 为真(落点是对方老将) -> 则 E2(移动)且 E4(获胜)。
通过绘制这样一张因果图,所有复杂的业务规则都被转化为了直观的逻辑电路图。这不仅帮助我们自己和团队理解需求,更是后续生成无遗漏测试用例的“蓝图”。在实际项目中,我经常用白板或绘图工具与产品经理、开发同学一起绘制和确认这张图,这个过程本身就能发现不少需求歧义点。
3. 第三步:转化决策表,穷举所有逻辑组合
因果图是理解逻辑的利器,但直接用它来写测试用例还不够系统。我们需要将其转化为决策表,这是一个将所有输入条件组合与对应输出动作系统化列出的表格。决策表能确保我们覆盖所有可能的逻辑路径。
根据上一步的因果图,特别是其中的约束条件,我们可以开始构建决策表。由于C4、C5、C6有异约束(E),所以它们在同一列中不能有多于一个为“真”(1)。同时,中间节点11(基本移动条件满足)是一个关键的分水岭。
为了清晰,我们通常将决策表分为两部分:一部分处理移动条件不满足(即E1发生)的情况;另一部分处理移动条件满足(即11为真)时,根据落点状态决定E2、E3、E4的情况。
下面是一个简化的决策表示例(“1”表示条件为真或动作发生,“0”表示假或不发生,“-”表示不关心):
表A:移动条件不满足的情况(E1发生)
| 条件/动作 | 情况1 | 情况2 | 情况3 | 情况4 |
|---|---|---|---|---|
| C1: 棋盘内 | 0 | 1 | 1 | 1 |
| C2: 日字型 | - | 0 | 1 | 1 |
| C3: 无绊马腿 | - | - | 0 | 1 |
| C7: 落点有己方棋子 | - | - | - | 1 |
| 中间节点11 | 0 | 0 | 0 | 0 |
| E1: 不移动 | 1 | 1 | 1 | 1 |
| E2: 移动 | 0 | 0 | 0 | 0 |
| E3: 吃子 | 0 | 0 | 0 | 0 |
| E4: 获胜 | 0 | 0 | 0 | 0 |
表B:移动条件满足的情况(11为真)
| 条件/动作 | 情况5 | 情况6 | 情况7 |
|---|---|---|---|
| C1: 棋盘内 | 1 | 1 | 1 |
| C2: 日字型 | 1 | 1 | 1 |
| C3: 无绊马腿 | 1 | 1 | 1 |
| C7: 落点有己方棋子 | 0 | 0 | 0 |
| 中间节点11 | 1 | 1 | 1 |
| C4: 落点无子 | 1 | 0 | 0 |
| C5: 对方非老将 | 0 | 1 | 0 |
| C6: 对方老将 | 0 | 0 | 1 |
| E1: 不移动 | 0 | 0 | 0 |
| E2: 移动 | 1 | 1 | 1 |
| E3: 吃子 | 0 | 1 | 0 |
| E4: 获胜 | 0 | 0 | 1 |
通过决策表,我们清晰地看到了7种主要的逻辑组合(有些无效组合已被约束排除)。这比凭空想象测试场景要全面和严谨得多。在实际操作中,你可能会发现某些列可以合并(如当C1为0时,其他条件已无关紧要),这能进一步简化用例数量。
4. 第四步:基于决策表,设计具体测试用例
决策表中的每一列(除开那些因约束而不可能存在的组合),都对应一个测试场景。我们的任务就是将这些抽象的逻辑场景,实例化为具体的、可执行的测试用例。
一个完整的测试用例通常包含:用例编号、测试标题、前置条件、测试步骤、输入数据、预期结果。这里,输入数据就是棋盘和棋子的具体状态。
我们需要为决策表中的每一列设计至少一个测试用例。例如,针对表B的情况6(移动条件满足,且落点为对方非老将):
- 用例ID: TC_MOVE_CAPTURE_01
- 测试标题: 马合法移动并吃掉对方非老将棋子
- 前置条件: 红方先手,棋盘处于开局或某个特定中局状态。
- 测试步骤:
- 摆放红马于特定位置(例如,坐标e5)。
- 在红马可走的“日”字点且无绊马腿的位置(例如g4),放置一个黑方棋子(如黑卒)。
- 执行“红马从e5移动到g4”的操作。
- 预期结果:
- 红马成功移动至g4位置。
- 原g4位置的黑卒被从棋盘上移除。
- 棋盘状态更新正确,游戏继续。
为了更全面地覆盖,我们还需要考虑边界和异常。例如,针对表A的情况1(落点在棋盘外):
- 用例ID: TC_MOVE_INVALID_BOARD_01
- 测试标题: 尝试移动马至棋盘外坐标
- 前置条件: 红马位于边线(例如a1)。
- 测试步骤: 尝试将红马从a1移动到棋盘外的坐标(例如,向左上移动“日”字到虚拟坐标
(-2, 0)或对应无效表示)。 - 预期结果: 系统应拒绝此次移动,棋子位置不变,并可能给出友好提示(如“落点无效”)。
我们可以用一个表格来汇总从决策表衍生出的核心测试用例:
| 用例编号 | 对应决策表列 | 测试场景简述 | 关键输入条件 | 预期输出结果 |
|---|---|---|---|---|
| TC_MOVE_INVALID_01 | 表A-情况1 | 起点在边线,尝试移出棋盘 | C1=0 (棋盘外) | E1: 不移动 |
| TC_MOVE_INVALID_02 | 表A-情况2 | 尝试走非“日”字 | C1=1, C2=0 | E1: 不移动 |
| TC_MOVE_INVALID_03 | 表A-情况3 | 路径被“绊马腿” | C1=1, C2=1, C3=0 | E1: 不移动 |
| TC_MOVE_INVALID_04 | 表A-情况4 | 落点有己方棋子 | C1=1, C2=1, C3=1, C7=1 | E1: 不移动 |
| TC_MOVE_VALID_01 | 表B-情况5 | 移动至空白点 | C4=1 | E2: 移动 |
| TC_MOVE_CAPTURE_01 | 表B-情况6 | 移动并吃对方非老将 | C5=1 | E2 & E3: 移动并吃子 |
| TC_MOVE_WIN_01 | 表B-情况7 | 移动并吃对方老将获胜 | C6=1 | E2 & E4: 移动并获胜 |
5. 第五步:优化、评审与落地执行
生成测试用例草案后,工作并未结束。最后一步是打磨和落实这些用例,确保其有效性和可执行性。
1. 用例优化与补充:
- 合并与简化:检查是否有预期结果完全相同的用例可以合并。例如,多种导致“不移动”的无效输入,如果系统处理方式一致(都是静默拒绝或同一提示),可以考虑合并为一个用例,用多组数据来测试。
- 补充边界值:在合法移动的用例中,要特别加入边界情况。例如,马在棋盘四个角落、紧贴边线、被多个棋子包围时的移动。尤其是“绊马腿”规则,需要测试马的前、后、左、右四个方向被挡住的不同情况。
- 补充异常和非法输入:考虑输入坐标格式错误、传入非法的棋子标识、在非本方回合移动马等情况。虽然因果图主要针对业务逻辑,但一个健壮的系统也需要处理这些异常。
2. 用例评审: 召集项目组成员(产品、开发、测试)进行用例评审。重点确认:
- 逻辑覆盖是否完整:基于因果图和决策表,大家共同审视是否所有逻辑分支都已覆盖。
- 预期结果是否正确:开发人员可以确认在某种输入组合下,系统的预期行为是否与产品需求一致。
- 可执行性:测试数据是否易于准备,操作步骤是否清晰无歧义。
3. 落地执行与自动化考量: 将评审后的测试用例录入测试管理工具。对于象棋走马这类规则清晰、输入输出明确的功能,非常适合进行自动化测试。我们可以编写脚本,自动设置棋盘初始状态,执行移动操作,并断言最终的棋盘状态是否符合预期。
# 示例:使用Python和pytest框架测试“马吃子”场景
import pytest
class TestChineseChessHorse:
def setup_method(self):
# 初始化一个棋盘对象,这里用简化字典表示
self.board = {
'e5': 'red_horse',
'g4': 'black_pawn',
# ... 其他棋子
}
def test_horse_captures_opponent_piece(self):
# 对应 TC_MOVE_CAPTURE_01
move_from = 'e5'
move_to = 'g4'
# 执行移动逻辑(调用被测函数)
result = make_move(self.board, move_from, move_to, player='red')
# 断言
assert result['success'] is True
assert self.board.get(move_from) is None # 起点清空
assert self.board.get(move_to) == 'red_horse' # 终点是红马
# 黑卒应被移除,不在棋盘任何位置
assert 'black_pawn' not in self.board.values()
assert result['game_over'] is False
通过这五个步骤——从规则分析到因果图,再到决策表,最后到具体用例设计和优化——我们就能系统化、无遗漏地完成对复杂逻辑功能的测试设计。因果图法强迫我们深入思考每一个条件之间的相互作用,这正是它相对于随意设计用例的最大价值。下次当你面对一个满是“if-else”的业务规则时,不妨试试这个方法,它很可能帮你发现那些隐藏在最深处的逻辑漏洞。
更多推荐
所有评论(0)