pytest参数化测试报错,如何实现多游戏测试用例分组执行?
问题背景
作为单元测试新手,我需要让test_select_childnode测试用例适配不同游戏场景:每个游戏需要创建对应的node和mcts实例,搭配整数参数执行测试。尝试用@pytest.mark.parametrize实现复用,但当前写法运行报错。之前直接将Fixture传入测试函数可以正常执行,但每个游戏都要重复编写测试函数和Fixture,存在大量冗余代码。
错误原因
当前代码中,直接将Fixture函数(如mcts_ttt、node_ttt)放入@pytest.mark.parametrize的参数列表中,pytest不会自动执行这些Fixture,而是将函数对象本身传递给测试函数。因此当代码访问mcts.nodes时,mcts是一个函数对象而非MCTS实例,自然会抛出AttributeError: 'function' object has no attribute 'nodes'。
解决方案
方法1:使用pytest-lazy-fixture延迟执行Fixture
这是最直接的修改方式,通过lazy_fixture引用Fixture,让pytest在参数化时正确执行Fixture并返回实例。
步骤:
- 安装依赖:
pip install pytest-lazy-fixture
- 修改参数化代码:
在@pytest.mark.parametrize中用lazy_fixture("fixture_name")替代直接传入Fixture函数。
修改后的代码:
import pytest from pytest_lazyfixture import lazy_fixture @pytest.fixture def node_ttt(): g = TicTacToeGame() node = Node(g) return node @pytest.fixture def mcts_ttt(): g = TicTacToeGame() nnet = TicTacToeNNet(g, nn_args) model = NNetWrapper(g, nnet, nn_args) mcts_instance = MCTS(model, mcts_args) return mcts_instance @pytest.mark.parametrize("mcts, node, action_idx", [ (lazy_fixture("mcts_ttt"), lazy_fixture("node_ttt"), 0), # 后续添加其他游戏的Fixture,比如lazy_fixture("mcts_hive"), lazy_fixture("node_hive"), 1 ]) def test_select_childnode(mcts, node, action_idx): """ Assert that childnode is creating a new node when needed. Assert that childnode is not creating a new node when not needed. """ mcts.nodes[node.id] = node child_node = mcts.select_childnode(node, action_idx) child_node2 = mcts.select_childnode(node, action_idx) assert child_node != child_node2, "child nodes are not unique when they should be" mcts.add_node(child_node, node.id, action_idx) child_node2 = mcts.select_childnode(node, action_idx) assert child_node == child_node2, "accessing the same child node that we previously added, should not create a new node"
方法2:抽象通用Fixture(更适合多游戏扩展)
如果需要支持多个游戏,推荐这种方式:将游戏类型作为参数,编写通用Fixture根据参数生成对应实例,避免重复编写多个类似的Fixture。
示例代码:
import pytest # 定义测试参数:每个元素为(游戏类型, action_idx) GAME_TEST_CASES = [ (TicTacToeGame, 0), # 后续添加其他游戏,比如(HiveGame, 1) ] @pytest.fixture def game(request): # 根据参数返回对应的游戏实例 game_class = request.param return game_class() @pytest.fixture def node(game): # 基于游戏实例生成node return Node(game) @pytest.fixture def mcts(game): # 基于游戏实例生成mcts nnet = game.nnet_class(game, nn_args) # 假设每个游戏类有nnet_class属性指向对应的网络类 model = NNetWrapper(game, nnet, nn_args) return MCTS(model, mcts_args) # 参数化时传入游戏类型和action_idx,同时指定game fixture的参数 @pytest.mark.parametrize("game, action_idx", GAME_TEST_CASES, indirect=["game"]) def test_select_childnode(mcts, node, action_idx): """ Assert that childnode is creating a new node when needed. Assert that childnode is not creating a new node when not needed. """ mcts.nodes[node.id] = node child_node = mcts.select_childnode(node, action_idx) child_node2 = mcts.select_childnode(node, action_idx) assert child_node != child_node2, "child nodes are not unique when they should be" mcts.add_node(child_node, node.id, action_idx) child_node2 = mcts.select_childnode(node, action_idx) assert child_node == child_node2, "accessing the same child node that we previously added, should not create a new node"
注:需要确保每个游戏类(如
TicTacToeGame)定义了对应的网络类属性nnet_class,或者根据实际情况调整通用mctsFixture的实现。
验证结果
修改后,pytest会正确执行Fixture生成对应的实例,test_select_childnode可以复用在不同游戏场景中,无需重复编写测试函数和Fixture,解决了冗余代码问题,同时避免了原有的AttributeError。
内容的提问来源于stack exchange,提问作者Tue

