中国象棋走马规则测试实战:用因果图法设计测试用例的5个关键步骤

作为一名在软件测试领域摸爬滚打了多年的工程师,我常常遇到一些逻辑复杂、条件交织的功能点,让人感觉测试用例设计起来无从下手。比如,一个看似简单的“中国象棋中马的走法”,背后就隐藏着棋盘边界、日字型移动、绊马腿、棋子碰撞等多重规则的组合。面对这种“牵一发而动全身”的输入输出关系,传统的等价类划分或边界值分析往往显得力不从心,容易遗漏那些由多个条件组合触发的隐蔽缺陷。

这时,因果图法就成了我们测试工具箱里的一把“瑞士军刀”。它不直接处理单个输入,而是专注于梳理原因(输入条件)与结果(输出动作)之间错综复杂的逻辑关系,尤其擅长处理那些包含约束条件(比如某些条件不能同时成立)的场景。今天,我就以“中国象棋走马”这个经典案例为蓝本,结合我自己的项目实战经验,为你拆解运用因果图法设计测试用例的五个核心步骤。无论你是刚入行的测试新手,还是想系统梳理黑盒测试方法的中级工程师,这篇文章都将带你走完一个从规则分析到用例落地的完整闭环。

1. 第一步:拆解规则,明确定义“因”与“果”

任何测试设计的第一步,都是深入理解需求。对于因果图法,这一步的目标是精准地识别出所有独立的“原因”和“结果”。这听起来简单,但实际操作中,模糊或错误的理解会导致后续全盘皆输。

原因,指的是所有可能影响系统行为的输入条件或前提。在象棋走马规则中,我们不能简单地把“马的走法”当成一个原因,而是要把它拆解成一系列原子级的、可被直接判定为“真”或“假”的输入条件。

结果,则是系统针对这些输入组合所做出的具体响应或输出动作。

让我们回到中国象棋的棋盘。马的走法规则,用自然语言描述通常如下:

  1. 如果落点在棋盘外,则不移动棋子。
  2. 如果落点与起点不构成“日”字型,则不移动棋子。
  3. 如果落点处有己方棋子,则不移动棋子。
  4. 如果落点方向的邻近交叉点有棋子(即“绊马腿”),则不移动棋子。
  5. 如果以上情况均不满足,且落点处无棋子,则移动棋子到该位置。
  6. 如果以上情况均不满足,且落点处为对方棋子(非老将),则移动棋子并吃掉对方棋子。
  7. 如果以上情况均不满足,且落点处为对方老将,则移动棋子,并提示战胜对方,游戏结束。

现在,我们需要将这些描述转化为测试语言。关键在于原子化。例如,“落点处有棋子”这个描述,需要进一步拆分为“落点处有己方棋子”和“落点处有对方棋子”两个独立的原因,因为它们的逻辑影响是不同的。

经过分析,我们可以梳理出以下原因和结果:

原因(输入条件):

  • 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: 棋盘内0111
C2: 日字型-011
C3: 无绊马腿--01
C7: 落点有己方棋子---1
中间节点110000
E1: 不移动1111
E2: 移动0000
E3: 吃子0000
E4: 获胜0000

表B:移动条件满足的情况(11为真)

条件/动作情况5情况6情况7
C1: 棋盘内111
C2: 日字型111
C3: 无绊马腿111
C7: 落点有己方棋子000
中间节点11111
C4: 落点无子100
C5: 对方非老将010
C6: 对方老将001
E1: 不移动000
E2: 移动111
E3: 吃子010
E4: 获胜001

通过决策表,我们清晰地看到了7种主要的逻辑组合(有些无效组合已被约束排除)。这比凭空想象测试场景要全面和严谨得多。在实际操作中,你可能会发现某些列可以合并(如当C1为0时,其他条件已无关紧要),这能进一步简化用例数量。

4. 第四步:基于决策表,设计具体测试用例

决策表中的每一列(除开那些因约束而不可能存在的组合),都对应一个测试场景。我们的任务就是将这些抽象的逻辑场景,实例化为具体的、可执行的测试用例。

一个完整的测试用例通常包含:用例编号、测试标题、前置条件、测试步骤、输入数据、预期结果。这里,输入数据就是棋盘和棋子的具体状态。

我们需要为决策表中的每一列设计至少一个测试用例。例如,针对表B的情况6(移动条件满足,且落点为对方非老将):

  • 用例ID: TC_MOVE_CAPTURE_01
  • 测试标题: 马合法移动并吃掉对方非老将棋子
  • 前置条件: 红方先手,棋盘处于开局或某个特定中局状态。
  • 测试步骤:
    1. 摆放红马于特定位置(例如,坐标e5)。
    2. 在红马可走的“日”字点且无绊马腿的位置(例如g4),放置一个黑方棋子(如黑卒)。
    3. 执行“红马从e5移动到g4”的操作。
  • 预期结果:
    1. 红马成功移动至g4位置。
    2. 原g4位置的黑卒被从棋盘上移除。
    3. 棋盘状态更新正确,游戏继续。

为了更全面地覆盖,我们还需要考虑边界和异常。例如,针对表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=0E1: 不移动
TC_MOVE_INVALID_03表A-情况3路径被“绊马腿”C1=1, C2=1, C3=0E1: 不移动
TC_MOVE_INVALID_04表A-情况4落点有己方棋子C1=1, C2=1, C3=1, C7=1E1: 不移动
TC_MOVE_VALID_01表B-情况5移动至空白点C4=1E2: 移动
TC_MOVE_CAPTURE_01表B-情况6移动并吃对方非老将C5=1E2 & E3: 移动并吃子
TC_MOVE_WIN_01表B-情况7移动并吃对方老将获胜C6=1E2 & 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”的业务规则时,不妨试试这个方法,它很可能帮你发现那些隐藏在最深处的逻辑漏洞。

更多推荐