如何为使用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
相关产品推荐
相关产品推荐

