You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为使用Pygame的四子棋脚本编写单元测试?

没问题!针对你用Pygame写的四子棋play方法,我们可以避开难测试的GUI渲染部分,聚焦于可分离的逻辑点和可模拟的交互场景来写单元测试。下面是具体的思路和代码示例:

一、先做小改造:让代码更易测试

你的play方法已经做得不错——把核心逻辑(is_valid_move、make_move等)作为参数注入了,这很利于测试。我们再补充一个小细节:把turn改成类的实例变量(比如self.turn),这样测试时能直接验证回合切换是否正确。

改造后的开头部分大概是这样:

def play(self, board, players, is_valid_move, make_move, is_winning_move):
    """Method to play the game in GUI using pygame
    Note: When playing with AI a mouse click is required to trigger AI move
    Args:
        board (numpy.ndarray): Game board
        players (list): List of players
        is_valid_move (function): Move validator
        make_move (function): Makes move
        is_winning_move (function): Check for winning move"""
    self.turn = random.randint(0, len(players) - 1)  # 改成实例变量
    while True:
        # 后续逻辑不变
二、可测试的功能点及代码示例

我们用Python标准库的unittest和unittest.mock来编写测试,重点覆盖回合初始化、AI落子触发、回合切换这些逻辑。

1. 测试回合初始化的随机性范围

turn = random.randint(0, len(players)-1)这段代码,我们需要验证它的输出始终在合法的玩家索引范围内:

import unittest
from unittest.mock import patch
from your_game_module import Game  # 替换成你的游戏类所在模块

class TestFourInARow(unittest.TestCase):
    def test_turn_initialization_range(self):
        game = Game()
        players = [Mock(), Mock()]  # 用Mock模拟玩家对象
        
        # 连续测试10次,确保每次生成的turn都在0-1之间
        for _ in range(10):
            game.play(board=None, players=players,
                      is_valid_move=lambda x: True,
                      make_move=lambda x, y: 0,
                      is_winning_move=lambda: False)
            self.assertIn(game.turn, [0, 1])

    def test_turn_initialization_random_call(self):
        game = Game()
        players = [Mock(), Mock()]
        
        # Mock random.randint,验证它被正确调用
        with patch('random.randint') as mock_rand:
            mock_rand.return_value = 1
            game.play(board=None, players=players,
                      is_valid_move=lambda x: True,
                      make_move=lambda x, y: 0,
                      is_winning_move=lambda: False)
            
            mock_rand.assert_called_once_with(0, len(players)-1)

2. 测试AI落子的触发逻辑

当轮到AI玩家时,点击鼠标应该触发get_move()、is_valid_move()和make_move(),并且完成回合切换。我们可以模拟Pygame的鼠标点击事件,同时Mock掉渲染相关的方法避免弹出窗口:

import pygame
from unittest.mock import Mock, patch

class TestFourInARow(unittest.TestCase):
    def setUp(self):
        pygame.init()
        self.game = Game()
        # 模拟玩家对象
        self.ai_player = Mock()
        self.ai_player.name = "AI"
        self.ai_player.p_id = 2
        self.ai_player.get_move.return_value = 3  # 模拟AI选择第3列(0索引)
        self.human_player = Mock()
        self.human_player.name = "Human"
        self.human_player.p_id = 1
        self.players = [self.human_player, self.ai_player]
        
        # Mock掉所有渲染方法,避免实际绘制
        self.game.draw = Mock()
        self.game.draw_black_rec = Mock()
        self.game.draw_player_coin = Mock()

    def test_ai_move_triggered_on_click(self):
        is_valid_move = Mock(return_value=True)
        make_move = Mock(return_value=4)
        
        # 模拟鼠标点击事件和退出事件(避免无限循环)
        click_event = pygame.event.Event(pygame.MOUSEBUTTONDOWN)
        quit_event = pygame.event.Event(pygame.QUIT)
        
        # 让pygame.event.get()返回我们模拟的事件
        with patch('pygame.event.get') as mock_events:
            mock_events.return_value = [click_event, quit_event]
            # 强制让初始回合是AI
            self.game.turn = 1
            
            self.game.play(Mock(), self.players, is_valid_move, make_move, lambda: False)
            
            # 验证AI的get_move被调用
            self.ai_player.get_move.assert_called_once()
            # 验证合法性检查和落子方法被正确调用
            is_valid_move.assert_called_once_with(3)
            make_move.assert_called_once_with(3, self.ai_player.p_id)
            # 验证回合切换到人类玩家
            self.assertEqual(self.game.turn, 0)

    def tearDown(self):
        pygame.quit()

3. 测试人类玩家的列转换逻辑(可选)

如果你的代码里有从鼠标位置转换为棋盘列号的逻辑(比如get_col_from_mouse_pos(pos)),可以单独测试这个方法的边界情况:

def test_get_col_from_mouse_pos(self):
    game = Game()
    game.screen_width = 700  # 假设窗口宽700
    game.cols = 7  # 7列,每列宽100
    
    # 测试点击第3列中间位置(x=250)
    col = game.get_col_from_mouse_pos((250, 300))
    self.assertEqual(col, 2)
    # 测试点击最左边界
    col = game.get_col_from_mouse_pos((10, 300))
    self.assertEqual(col, 0)
    # 测试点击最右边界
    col = game.get_col_from_mouse_pos((690, 300))
    self.assertEqual(col, 6)
三、测试关键技巧总结
  • Mock一切外部依赖:Pygame的渲染、事件循环、random模块都可以用unittest.mock替换,让测试只关注核心逻辑。
  • 拆分GUI和业务逻辑:把落子判断、胜负检查、回合管理这些逻辑抽成独立方法,不要和Pygame的事件处理混在一起,测试会更简单。
  • 避免无限循环:测试时一定要让pygame.event.get()返回QUIT事件,否则play方法会一直运行,测试无法结束。

内容的提问来源于stack exchange,提问作者Kartikei Mittal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 05:36:22