如何从pytest注入pygame事件?

Zab*_*aba 5 python pygame pytest

如何从 pytest 测试模块向正在运行的 pygame 注入事件?

以下是一个 pygame 的最小示例,它在J按下时绘制一个白色矩形并在按下时退出游戏Ctrl-Q

#!/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()
Run Code Online (Sandbox Code Playgroud)

我想编写一个pytest可以自动测试它的模块。我读过可以将事件注入 running pygame在这里我读到yield from允许双向通信,所以我认为我必须实现某种pygame.eventspytest模块注入的钩子,但它并不像我想象的那么简单,所以我把它注释掉了。如果我取消注释下的测试钩子while game_runningpygame甚至不等待任何输入。

这是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)
Run Code Online (Sandbox Code Playgroud)

Zab*_*aba 3

Pygame 可以对自定义用户事件做出反应,而不是按键或鼠标事件。这是一个工作代码,其中pytest将用户事件发送到pygamepygame对其做出反应并将响应发送回pytest以进行评估:

#!/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()
Run Code Online (Sandbox Code Playgroud)

这是测试代码:

#!/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")
Run Code Online (Sandbox Code Playgroud)

它可以工作,但是,pytest作为无状态单元测试而不是集成测试的工具,使得 pygame 在获得第一个响应(拆卸测试)后退出。在当前 pygame 会话中不可能继续进行更多测试和断言。(只要尝试复制测试代码的最后两行来重新发送事件,就会失败。)Pytest 不是向 pygame 注入一系列指令以使其达到前提条件然后执行一系列测试的正确工具。

至少这是我从 pygame Discord 频道上的人们那里听到的。对于自动化集成测试,他们建议使用 BDD 工具,例如Cucumber(或适用于 python)。