2

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 Answers2

0

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())
Expurple
  • 877
  • 6
  • 13
  • Using a file makes sense. I will do that for the time being. For your beyond scope section, I believe you that it is better, but aside from not mocking inputs, what is the purpose of creating a wrapper versus just implementing the engine in Game? If I called process_input I would have to calculate what that input is supposed to be telling the game to do, depending on current game state. Is that correct? E.g I would need to calculate what menu was last displayed and for who for each input. That feels like a lot of added complexity, so what is the gain? I would love an external resource to read. – ChangelingX Jan 04 '22 at 15:06
  • Don't you already have a similar system somewhere inside of `Game`? I mean the loop where you wait for the next portion of input and then mutate state based on that – Expurple Jan 04 '22 at 15:15
  • Yeah. I have, for example, `create_player(self): self.players[0] = input("enter player name")` (more than that but you get the idea), which is called within `Game.setup_game()` which is called by `Game.run()` after New Game is selected from the first menu. I am unsure what I gain from pulling all of that out and creating process_input to handle the branching instead, beyond not having to mock inputs. I guess it is cleaner? I am currently looking at doing it the way you've suggested, so I'm sure it will make sense once I actually work with it. Now I'm stuck on the "how", however. – ChangelingX Jan 04 '22 at 15:33
  • `process_input()` in my example isn't supposed to handle stuff like player names. It's supposed to handle the in-game commands. I'd get the player names somewhrere up the stack: `engine.players[0] = input("enter player name")`, inside of something like `GameIO.setup_game()` (`GameIO` owns a `GameEngine` instance and provides input to it). Menus should be part of `GameIO` instead of `GameEngine`, and call specialized `GameEngine` methods like `.set_player_name()` (not a single `process_input()` that encapsulates everything) – Expurple Jan 04 '22 at 15:43
  • That makes more sense. I will look at this further and see what I can come up with. Thank you for your answer and additional help. – ChangelingX Jan 04 '22 at 15:48
  • If you completely move input and menus out of the engine, you'll even be able to reuse it with a GUI – Expurple Jan 04 '22 at 17:03
  • In games, I've only done toy CLIs like yours (not even curses), so I can't point you at resources with better explanation of this principle. For me, it's just separation of logic and IO, seems natural in any project. I remember reading something about this idea in [The Art Of Unix Programming](http://www.catb.org/esr/writings/taoup/html/) – Expurple Jan 04 '22 at 18:31
  • Yeah. I am actually doing it now and it makes sense to me. Thank you for the correction. I am just screwing up as I go and learning from the headaches, so thanks for making my future 'oh god why' smaller. The unit testing is easier to figure out this way, too. – ChangelingX Jan 04 '22 at 23:55
  • Nice to see my offtopic advice actually work out as a solution to XY problem. Consider marking as accepted then – Expurple Jan 05 '22 at 01:02
0

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
match
  • 10,388
  • 3
  • 23
  • 41