Efficient ways to pass a large amount of inputs in unittest

Viewed 162

I am currently writing a little Python adaptation of Rummy 500 to (re)familiarize myself with the language and unittest.

I have most of my application written and running, and now it is time to test the actual game flow.

I believe my final unittest will actually simulate a given game from start to finish and confirm that the state matches what is expected during / after the game resolves.

My plan is to use mock to pass a long series of inputs to the game in order to non-interactively test that the game works interactively. I believe I can just do something like the answer suggested here: Mock user input() but with a very long array for lines like

@mock.patch('builtins.input', side_effect=['11', '13', 'Bob'])

I am sure that this will work, but I can see it becoming an unwieldy mess as the input list gets longer.

Is there a better way to accomplish this same goal? My naive thought is to have a series of unit test suites that build on each other so I can just add to an array of inputs as I test each stage of the game, one after another.

E.g.

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

repeat ad nauseum until I finish the game.

I believe this would work and would be both readable (as can be, at least) and maintainable, but I want to think there's an easier way. I am not concerned with the flow within the tests, I can muddle through that myself, but if there is a better way to do this, I would love to know.

2 Answers

If test_second_turn() also interprets inputs_first_turn, why is test_first_turn() necessary? You're essentially testing the same inputs twice. I'd just have a single test method, where you cycle through the whole input, turn by turn. If the method becomes too large, extract subroutines that test the existing Game instance without creating a new one.

For mocking the whole input at once, you could put it in a multiline string literal or a separate file like test_input.txt, and then mock input() by replacing it with something like this:

input_file = open('test_input.txt', 'r')
# On each call returns the next line of input from file
input = lambda: next(input_file)

I'm not familiar with mocking input in Python specifically, you'll probably need to use some decorator instead of input =. But you get the idea, put all inputs in one file/string, and mock input with that.

Architecture

This is out of the scope of your question, but ideally, you shouldn't have to mock input() to test the game state. Instead, I usually do something like this:

class GameEngine:
    def process_input(input: str):
        # game logic here

And test this class by providing input strings directly.

Game then becomes just a thin IO wrapper that does something like this:

class Game:
    def run(self):
        engine = GameEngine()
        while not engine.has_finished:
            engine.process_input(input())
            print(engine.get_output())

One way to achieve this is to use the ddt module which lets you parametrize unittest and call the same test repeatedly with different data.

This example shows how to create a generator which returns a bigger slice of the full data each time. Just modify gamedata to yield your turns:

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
Related