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)
for event in pygame. event. get() handles the internal events an retrieves a list of external events (the events are removed from the internal event queue). If you press the close button of the window, than the causes the QUIT event and you'll get the event by for event in pygame.
Pygame event loop: pygame. event. get() returns a list with all unprocessed events.
event. pump() to allow pygame to handle internal actions. This function is not necessary if your program is consistently processing events on the queue through the other pygame. event pygame module for interacting with events and queues functions.
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With