Jak uczynić grę wygodną do testów automatycznych
Czyste funkcje, Game(rng=...) a uruchamianie bez okna to gra, którą można sprawdzić automatycznie.
Gry wielkości tego projektu nie da się sprawdzić tylko ręcznym uruchomieniem — zbyt dużo kombinacji stanów. Ostateczna wersja napisana jest tak, aby można ją było testować automatycznie, bez żadnego rzeczywistego okna.
Funkcje czyste: logika bez Pygame
Formuły trudności (rozdział 21.17), współrzędna pojawienia się wroga (rozdział 21.14) i liczenie punktów (rozdział 21.15) — zwykłe funkcje od liczb i list, bez odwoływania się do
self lub ekran:
def ochki_za_unichtozhennyh(vragi):
return sum(vrag.points for vrag in vragi)
# Czysta funkcja nie potrzebuje prawdziwego Enemy ze sprite’em i obrazkiem —
# każdy obiekt posiadający atrybut points: jest wystarczający
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
To nie obejście reguł, ale bezpośredni skutek tego, że ochki_za_unichtozhennyh
w ogóle nie sprawdza, jakiej klasy są przekazane obiekty — potrzebuje tylko atrybutu
.points dla wszystkich. Taka funkcja może być testowana przez każdego
obiekt z tym atrybutem, aż do prostego stubu, bez rozpoczynania Pygame i bez tworzenia
prawdziwi wrogowie.
Sterowany przypadek: Game(rng=...)
Gra przyjmuje generator liczb losowych jako jawny parametr, a nie odwołuje się do globalnego
random bezpośrednio:
class Game:
def __init__(self, *, rng=None):
self.rng = rng if rng is not None else random.Random()
# w prawdziwej grze to typowa nieprzewidywalna losowość:
game = Game()
# w testach i przy tworzeniu zrzutów ekranu – powtarzalne:
game = Game(rng=random.Random(42))
random.Random(42) z ustalonym ziarnem (seed) daje tę samą sekwencję losowych liczb przy każdym starcie – dzięki temu pojawienie się wrogów, ich typ i pozycja stają się powtarzalne: wygodne zarówno w testach, jak i scenariuszach deterministycznych – na przykład dla zrzutów ekranu z konkretnego, wcześniej znanego stanu gry.Uruchamianie bez okien: Pusty sterownik SDL- (headless)
Ten sam trik co we wszystkich testach rozdziału 20: zmienne środowiskowe przełączają Pygame na "pusty" (dummy) sterownik wideo i audio SDL – okno fizycznie nie pojawia się nigdzie indziej, lecz całe Logika (Surface, Rect, kolizje, dźwięk) działa naprawdę. Taki start bez Realne okno w terminologii angielskiej nazywa się headless – "bez głowy", czyli bez ekran i brak interfejsu użytkownika.
import os
os.environ.setdefault("SDL_VIDEODRIVER", "dummy")
os.environ.setdefault("SDL_AUDIODRIVER", "dummy")
import space_shooter as ss
game = ss.Game() # prawdziwa Game, brak prawdziwego okna
space_shooter.py nie powoduje pygame.init() lub Game().run() na poziomie modułu — cała inicjalizacja odbywa się wewnętrznie Game.__init__(), ale nieskończona pętla występuje tylko wewnątrz if __name__ == "__main__":. W przeciwnym razie to po prostu import space_shooter w teście prowadziłby prawdziwą grę.