在单元测试中传递大量输入的有效方法

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