在单元测试中传递大量输入的有效方法
Efficient ways to pass a large amount of inputs in unittest
我目前正在编写 Rummy 500 的一些 Python 改编,以(重新)熟悉语言和单元测试。
我已经编写了大部分应用程序 运行,现在是测试实际游戏流程的时候了。
我相信我的最终单元测试实际上会从头到尾模拟给定的游戏,并确认状态与游戏解决期间/之后的预期相符。
我的计划是使用 mock 将一长串输入传递给游戏,以便以非交互方式测试游戏是否可以交互运行。我相信我可以像这里建议的答案那样做一些事情: 但是对于像
这样的行有一个很长的数组
@mock.patch('builtins.input', side_effect=['11', '13', 'Bob'])
我相信这会奏效,但我可以看到随着输入列表变长,它会变得笨拙。
有没有更好的方法来实现同样的目标?我天真的想法是拥有一系列相互构建的单元测试套件,这样我就可以在测试游戏的每个阶段时添加一个输入数组,一个接一个。
例如
inputs_first_turn = ['Player 1', 'F', 1, 2, 1, 5, 'Player 2', 'M', 1, 3, 2, 5]
@mock.patch('builtins.input', side_effect=inputs_first_turn)
def test_first_turn(self, input):
game = Game()
# tests on game state go here
inputs_second_turn = inputs_first_turn + [3, 1, 2, 2, 5, 3, 3, 2, 4, 5]
@mock.patch('builtins.input', side_effect=inputs_second_turn)
def test_second_turn(self, input):
game = Game()
#tests on game state go here
重复广告直到我完成游戏。
我相信这会奏效,并且既可读(至少可以)又可维护,但我认为有更简单的方法。我不关心测试中的流程,我可以自己蒙混过关,但如果有更好的方法,我很想知道。
如果test_second_turn()
也解释inputs_first_turn
,为什么test_first_turn()
是必要的?您实际上是在测试相同的输入两次。我只有一个测试方法,您可以在其中循环遍历整个输入,依次进行。如果该方法变得太大,请提取子例程来测试现有 Game
实例而不创建新实例。
为了一次模拟整个输入,您可以将其放在多行字符串文字或单独的文件中,例如 test_input.txt
,然后通过将其替换为如下内容来模拟 input()
:
input_file = open('test_input.txt', 'r')
# On each call returns the next line of input from file
input = lambda: next(input_file)
我不熟悉 Python 中的模拟输入,具体而言,您可能需要使用一些装饰器而不是 input =
。但是你明白了,将所有输入放在一个 file/string 中,然后用它模拟 input
。
建筑
这超出了您的问题范围,但理想情况下,您不必模拟 input()
来测试游戏状态。相反,我通常会这样做:
class GameEngine:
def process_input(input: str):
# game logic here
并通过直接提供输入字符串来测试 class。
Game
然后变成一个瘦的 IO 包装器,做这样的事情:
class Game:
def run(self):
engine = GameEngine()
while not engine.has_finished:
engine.process_input(input())
print(engine.get_output())
实现此目的的一种方法是使用 ddt
模块,它允许您参数化 unittest
并使用不同的数据重复调用相同的测试。
此示例展示了如何创建一个生成器,该生成器每次 returns 完整数据的较大部分。只需修改 gamedata
即可让出你的回合:
import unittest
from ddt import ddt, idata
def gamedata():
fullgame = [[0,1], [2, 3], [4, 5]]
for i in range(1, len(fullgame)+1):
yield fullgame[:i]
@ddt
class TestFoo(unittest.TestCase):
@idata(gamedata())
def test_foo(self, data):
print('DATA:', data)
# Replace with real tests
assert True
if __name__ == "__main__":
unittest.main()
DATA: [[0, 1]]
.DATA: [[0, 1], [2, 3]]
.DATA: [[0, 1], [2, 3], [4, 5]]
.DATA: [[0, 1], [2, 3], [4, 5], [6, 7]]
.
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK
我目前正在编写 Rummy 500 的一些 Python 改编,以(重新)熟悉语言和单元测试。
我已经编写了大部分应用程序 运行,现在是测试实际游戏流程的时候了。
我相信我的最终单元测试实际上会从头到尾模拟给定的游戏,并确认状态与游戏解决期间/之后的预期相符。
我的计划是使用 mock 将一长串输入传递给游戏,以便以非交互方式测试游戏是否可以交互运行。我相信我可以像这里建议的答案那样做一些事情:
@mock.patch('builtins.input', side_effect=['11', '13', 'Bob'])
我相信这会奏效,但我可以看到随着输入列表变长,它会变得笨拙。
有没有更好的方法来实现同样的目标?我天真的想法是拥有一系列相互构建的单元测试套件,这样我就可以在测试游戏的每个阶段时添加一个输入数组,一个接一个。
例如
inputs_first_turn = ['Player 1', 'F', 1, 2, 1, 5, 'Player 2', 'M', 1, 3, 2, 5]
@mock.patch('builtins.input', side_effect=inputs_first_turn)
def test_first_turn(self, input):
game = Game()
# tests on game state go here
inputs_second_turn = inputs_first_turn + [3, 1, 2, 2, 5, 3, 3, 2, 4, 5]
@mock.patch('builtins.input', side_effect=inputs_second_turn)
def test_second_turn(self, input):
game = Game()
#tests on game state go here
重复广告直到我完成游戏。
我相信这会奏效,并且既可读(至少可以)又可维护,但我认为有更简单的方法。我不关心测试中的流程,我可以自己蒙混过关,但如果有更好的方法,我很想知道。
如果test_second_turn()
也解释inputs_first_turn
,为什么test_first_turn()
是必要的?您实际上是在测试相同的输入两次。我只有一个测试方法,您可以在其中循环遍历整个输入,依次进行。如果该方法变得太大,请提取子例程来测试现有 Game
实例而不创建新实例。
为了一次模拟整个输入,您可以将其放在多行字符串文字或单独的文件中,例如 test_input.txt
,然后通过将其替换为如下内容来模拟 input()
:
input_file = open('test_input.txt', 'r')
# On each call returns the next line of input from file
input = lambda: next(input_file)
我不熟悉 Python 中的模拟输入,具体而言,您可能需要使用一些装饰器而不是 input =
。但是你明白了,将所有输入放在一个 file/string 中,然后用它模拟 input
。
建筑
这超出了您的问题范围,但理想情况下,您不必模拟 input()
来测试游戏状态。相反,我通常会这样做:
class GameEngine:
def process_input(input: str):
# game logic here
并通过直接提供输入字符串来测试 class。
Game
然后变成一个瘦的 IO 包装器,做这样的事情:
class Game:
def run(self):
engine = GameEngine()
while not engine.has_finished:
engine.process_input(input())
print(engine.get_output())
实现此目的的一种方法是使用 ddt
模块,它允许您参数化 unittest
并使用不同的数据重复调用相同的测试。
此示例展示了如何创建一个生成器,该生成器每次 returns 完整数据的较大部分。只需修改 gamedata
即可让出你的回合:
import unittest
from ddt import ddt, idata
def gamedata():
fullgame = [[0,1], [2, 3], [4, 5]]
for i in range(1, len(fullgame)+1):
yield fullgame[:i]
@ddt
class TestFoo(unittest.TestCase):
@idata(gamedata())
def test_foo(self, data):
print('DATA:', data)
# Replace with real tests
assert True
if __name__ == "__main__":
unittest.main()
DATA: [[0, 1]]
.DATA: [[0, 1], [2, 3]]
.DATA: [[0, 1], [2, 3], [4, 5]]
.DATA: [[0, 1], [2, 3], [4, 5], [6, 7]]
.
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK