How to inject pygame events from pytest?

How can one inject events into a running pygame from a pytest test module?

The following is a minimal example of a pygame which draws a white rectangle when J is pressed and quits the game when Ctrl-Q is pressed.

#!/usr/bin/env python
"""minimal_pygame.py"""

import pygame


def minimal_pygame(testing: bool=False):
    pygame.init()
    game_window_sf = pygame.display.set_mode(
            size=(400, 300), 
        )
    pygame.display.flip()
    game_running = True
    while game_running:
        # Main game loop:
        # the following hook to inject events from pytest does not work:
        # if testing:
            # test_input = (yield)
            # pygame.event.post(test_input)
        for event in pygame.event.get():
            # React to closing the pygame window:
            if event.type == pygame.QUIT:
                game_running = False
                break
            # React to keypresses:
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_q:
                    # distinguish between Q and Ctrl-Q
                    mods = pygame.key.get_mods()
                    # End main loop if Ctrl-Q was pressed
                    if mods & pygame.KMOD_CTRL:
                        game_running = False
                        break
                # Draw a white square when key J is pressed:
                if event.key == pygame.K_j:
                    filled_rect = game_window_sf.fill(pygame.Color("white"), pygame.Rect(50, 50, 50, 50))
                    pygame.display.update([filled_rect])
    pygame.quit()


if __name__ == "__main__":
    minimal_pygame()

I want to write a pytest module which would automatically test it. I have read that one can inject events into running pygame. Here I read that yield from allows a bidirectional communication, so I thought I must implement some sort of a hook for pygame.events being injected from the pytest module, but it is not as simple as I thought, so I commented it out. If I uncomment the test hook under while game_running, pygame does not even wait for any input.

Here is the test module for pytest:

#!/usr/bin/env python
"""test_minimal_pygame.py"""

import pygame
import minimal_pygame


def pygame_wrapper(coro):
    yield from coro


def test_minimal_pygame():
    wrap = pygame_wrapper(minimal_pygame.minimal_pygame(testing=True))
    wrap.send(None) # prime the coroutine
    test_press_j = pygame.event.Event(pygame.KEYDOWN, {"key": pygame.K_j})
    for e in [test_press_j]:
        wrap.send(e)

1 answer

  • answered 2020-08-11 06:40 Zababa

    Pygame can react to custom user events, not keypress or mouse events. Here is a working code where pytest sends a userevent to pygame, pygame reacts to it and sends a response back to pytest for evaluation:

    #!/usr/bin/env python
    """minimal_pygame.py"""
    
    import pygame
    
    
    TESTEVENT = pygame.event.custom_type()
    
    
    def minimal_pygame(testing: bool=False):
        pygame.init()
        game_window_sf = pygame.display.set_mode(
                size=(400, 300), 
            )
        pygame.display.flip()
        game_running = True
        while game_running:
            # Hook for testing
            if testing:
                attr_dict = (yield)
                test_event = pygame.event.Event(TESTEVENT, attr_dict)
                pygame.event.post(test_event)
            # Main game loop:
            pygame.time.wait(1000)
            for event in pygame.event.get():
                # React to closing the pygame window:
                if event.type == pygame.QUIT:
                    game_running = False
                    break
                # React to keypresses:
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_q:
                        # distinguish between Q and Ctrl-Q
                        mods = pygame.key.get_mods()
                        # End main loop if Ctrl-Q was pressed
                        if mods & pygame.KMOD_CTRL:
                            game_running = False
                            break
                # React to TESTEVENTS:
                if event.type == TESTEVENT:
                    if event.instruction == "draw_rectangle":
                        filled_rect = game_window_sf.fill(pygame.Color("white"), pygame.Rect(50, 50, 50, 50))
                        pygame.display.update([filled_rect])
                        pygame.time.wait(1000)
                        if testing:
                            # Yield the color value of the pixel at (50, 50) back to pytest
                            yield game_window_sf.get_at((50, 50))
        pygame.quit()
    
    
    if __name__ == "__main__":
        minimal_pygame()
    

    Here's the test code:

    #!/usr/bin/env python
    """test_minimal_pygame.py"""
    
    import minimal_pygame
    import pygame
    
    
    def pygame_wrapper(coro):
        yield from coro
    
    
    def test_minimal_pygame():
        wrap = pygame_wrapper(minimal_pygame.minimal_pygame(testing=True))
        wrap.send(None) # prime the coroutine
        # Create a dictionary of attributes for the future TESTEVENT
        attr_dict = {"instruction": "draw_rectangle"}
        response = wrap.send(attr_dict)
        assert response == pygame.Color("white")
    

    It works, However, pytest, being a tool for stateless unit tests rather than integration tests, makes the pygame quit after it gets the first response (teardown test). It is not possible to continue and do more tests and assertions in the current pygame session. (Just try to duplicate the last two lines of the test code to resend the event, it will fail.) Pytest is not the right tool to inject a series of instructions into pygame to bring it to a precondition and then perform a series of tests.

    That's at least what I heard from people on the pygame discord channel. For automated integration tests they suggest a BDD tool like Cucumber (or behave for python).