Как сделать игру удобной для автоматических тестов
Чистые функции, Game(rng=...) и запуск без окна — игра, которую можно проверить автоматически.
Игру размером с этот проект нельзя проверить только запуском вручную — слишком много комбинаций состояний. Финальная версия написана так, чтобы её можно было тестировать автоматически, без единого настоящего окна.
Чистые функции: логика без Pygame
Формулы сложности (раздел 21.17), координата появления врага (раздел 21.14) и подсчёт
очков (раздел 21.15) — обычные функции от чисел и списков, без обращения к
self или экрану:
def ochki_za_unichtozhennyh(vragi):
return sum(vrag.points for vrag in vragi)
# Чистой функции не нужен настоящий Enemy со спрайтом и картинкой —
# достаточно любого объекта с атрибутом points:
class ZaglushkaVraga:
def __init__(self, points):
self.points = points
assert ochki_za_unichtozhennyh([]) == 0
assert ochki_za_unichtozhennyh(
[ZaglushkaVraga(points=100), ZaglushkaVraga(points=200)]
) == 300
Это не обход правил, а прямое следствие того, что ochki_za_unichtozhennyh
вообще не проверяет, какого класса переданные объекты — ей нужен только атрибут
.points у каждого. Такую функцию можно протестировать любым
объектом с этим атрибутом, вплоть до простой заглушки, без запуска Pygame и без создания
настоящих врагов.
Управляемая случайность: Game(rng=...)
Игра принимает генератор случайных чисел явным параметром, а не обращается к глобальному
random напрямую:
class Game:
def __init__(self, *, rng=None):
self.rng = rng if rng is not None else random.Random()
# в реальной игре — обычная непредсказуемая случайность:
game = Game()
# в тестах и при генерации скриншотов — воспроизводимая:
game = Game(rng=random.Random(42))
random.Random(42) с фиксированным зерном (seed) даёт одну и ту же последовательность случайных чисел при каждом запуске — поэтому появление врагов, их тип и позиция становятся воспроизводимыми: удобно и для тестов, и для детерминированных сценариев — например, для скриншотов, снятых с конкретного, заранее известного состояния игры.Запуск без окна: «пустой» SDL-драйвер (headless)
Тот же приём, что и во всех тестах главы 20: переменные окружения переключают Pygame на «пустой» (dummy) видео- и аудиодрайвер SDL — окно нигде физически не появляется, но вся логика (Surface, Rect, столкновения, звук) работает по-настоящему. Такой запуск без настоящего окна в англоязычной терминологии называют headless — «без головы», то есть без экрана и без пользовательского интерфейса.
import os
os.environ.setdefault("SDL_VIDEODRIVER", "dummy")
os.environ.setdefault("SDL_AUDIODRIVER", "dummy")
import space_shooter as ss
game = ss.Game() # реальный Game, без настоящего окна
space_shooter.py не вызывает pygame.init() или Game().run() на уровне модуля — вся инициализация происходит внутри Game.__init__(), а бесконечный цикл — только внутри if __name__ == "__main__":. Иначе просто import space_shooter в тесте запустил бы настоящую игру.