TL; DR Potrzebuję pomocy w identyfikacji technik upraszczających zautomatyzowane testowanie jednostek podczas pracy w ramach stanowych.
Tło:
Obecnie piszę grę w TypeScript i frameworku Phaser . Phaser opisuje się jako środowisko gry HTML5, które stara się jak najmniej ograniczać strukturę twojego kodu. Wiąże się to z kilkoma kompromisami, a mianowicie, że istnieje boski obiekt Phaser.Game, który umożliwia dostęp do wszystkiego: pamięci podręcznej, fizyki, stanów gry i innych.
Ta stan sprawia, że naprawdę trudno jest przetestować wiele funkcji, takich jak moja Tilemap. Zobaczmy przykład:
Tutaj sprawdzam, czy moje warstwy kafelków są prawidłowe, i mogę zidentyfikować ściany i stworzenia w mojej Tilemapie:
export class TilemapTest extends tsUnit.TestClass {
constructor() {
super();
this.map = this.mapLoader.load("maze", this.manifest, this.mazeMapDefinition);
this.parameterizeUnitTest(this.isWall,
[
[{ x: 0, y: 0 }, true],
[{ x: 1, y: 1 }, false],
[{ x: 1, y: 0 }, true],
[{ x: 0, y: 1 }, true],
[{ x: 2, y: 0 }, false],
[{ x: 1, y: 3 }, false],
[{ x: 6, y: 3 }, false]
]);
this.parameterizeUnitTest(this.isCreature,
[
[{ x: 0, y: 0 }, false],
[{ x: 2, y: 0 }, false],
[{ x: 1, y: 3 }, true],
[{ x: 4, y: 1 }, false],
[{ x: 8, y: 1 }, true],
[{ x: 11, y: 2 }, false],
[{ x: 6, y: 3 }, false]
]);
Niezależnie od tego, co zrobię, gdy tylko spróbuję utworzyć mapę, Phaser wewnętrznie wywołuje jej pamięć podręczną, która jest zapełniana tylko podczas działania.
Nie mogę wywołać tego testu bez załadowania całej gry.
Złożonym rozwiązaniem może być napisanie adaptera lub serwera proxy, który buduje mapę tylko wtedy, gdy musimy wyświetlić ją na ekranie. Albo mógłbym sam zapełnić grę, ręcznie ładując tylko zasoby, których potrzebuję, a następnie używając go tylko dla określonej klasy testowej lub modułu.
Wybrałem to, co uważam za bardziej pragmatyczne, ale obce rozwiązanie tego problemu. Pomiędzy ładowaniem mojej gry a faktycznym jej odtwarzaniem przeskoczyłem TestState
w, który uruchamia test ze wszystkimi zasobami i danymi z pamięci podręcznej już załadowanymi.
To jest fajne, ponieważ mogę przetestować wszystkie funkcje, których chcę, ale także odłączyć, ponieważ jest to techniczny test integracji i zastanawiam się, czy nie mogę po prostu spojrzeć na ekran i sprawdzić, czy wrogowie są wyświetlani. W rzeczywistości nie, mogli zostać błędnie zidentyfikowani jako Przedmiot (zdarzyło się to już raz) lub - później w testach - mogli nie otrzymać wydarzeń związanych z ich śmiercią.
Moje pytanie - czy shimming w stanie testowym jest taki powszechny? Czy istnieją lepsze podejścia, zwłaszcza w środowisku JavaScript, o których nie wiem?
Inny przykład:
OK, oto bardziej konkretny przykład, który pomoże wyjaśnić, co się dzieje:
export class Tilemap extends Phaser.Tilemap {
// layers is already defined in Phaser.Tilemap, so we use tilemapLayers instead.
private tilemapLayers: TilemapLayers = {};
// A TileMap can have any number of layers, but
// we're only concerned about the existence of two.
// The collidables layer has the information about where
// a Player or Enemy can move to, and where he cannot.
private CollidablesLayer = "Collidables";
// Triggers are map events, anything from loading
// an item, enemy, or object, to triggers that are activated
// when the player moves toward it.
private TriggersLayer = "Triggers";
private items: Array<Phaser.Sprite> = [];
private creatures: Array<Phaser.Sprite> = [];
private interactables: Array<ActivatableObject> = [];
private triggers: Array<Trigger> = [];
constructor(json: TilemapData) {
// First
super(json.game, json.key);
// Second
json.tilesets.forEach((tileset) => this.addTilesetImage(tileset.name, tileset.key), this);
json.tileLayers.forEach((layer) => {
this.tilemapLayers[layer.name] = this.createLayer(layer.name);
}, this);
// Third
this.identifyTriggers();
this.tilemapLayers[this.CollidablesLayer].resizeWorld();
this.setCollisionBetween(1, 2, true, this.CollidablesLayer);
}
Skonstruowałem Tilemap z trzech części:
- Mapy
key
- Szczegółowy
manifest
opis wszystkich zasobów (arkuszy i arkuszy) wymaganych przez mapę - A,
mapDefinition
który opisuje strukturę i warstwy tilemapy.
Po pierwsze, muszę zadzwonić do super, aby zbudować Tilemap w Phaserze. Jest to część, która wywołuje wszystkie te połączenia do pamięci podręcznej, gdy próbuje wyszukać rzeczywiste zasoby, a nie tylko klucze zdefiniowane w pliku manifest
.
Po drugie, łączę arkusze i warstwy płytek z Tilemap. Może teraz renderować mapę.
Po trzecie, iterację moich warstw i znaleźć jakieś specjalne przedmioty, które chcę do wyciągnięcia z mapy: Creatures
, Items
, Interactables
i tak dalej. Te obiekty tworzę i przechowuję do późniejszego wykorzystania.
Obecnie nadal mam stosunkowo prosty interfejs API, który pozwala mi znajdować, usuwać i aktualizować te podmioty:
wallAt(at: TileCoordinates) {
var tile = this.getTile(at.x, at.y, this.CollidablesLayer);
return tile && tile.index != 0;
}
itemAt(at: TileCoordinates) {
return _.find(this.items, (item: Phaser.Sprite) => _.isEqual(this.toTileCoordinates(item), at));
}
interactableAt(at: TileCoordinates) {
return _.find(this.interactables, (object: ActivatableObject) => _.isEqual(this.toTileCoordinates(object), at));
}
creatureAt(at: TileCoordinates) {
return _.find(this.creatures, (creature: Phaser.Sprite) => _.isEqual(this.toTileCoordinates(creature), at));
}
triggerAt(at: TileCoordinates) {
return _.find(this.triggers, (trigger: Trigger) => _.isEqual(this.toTileCoordinates(trigger), at));
}
getTrigger(name: string) {
return _.find(this.triggers, { name: name });
}
Właśnie tę funkcjonalność chcę sprawdzić. Jeśli nie dodam warstw lub zestawów kafelków, mapa nie będzie renderowana, ale być może będę w stanie ją przetestować. Jednak nawet wywołanie super (...) wywołuje logikę kontekstową lub stanową, której nie mogę wyodrębnić w moich testach.
new Tilemap(...)
Phaser zaczyna kopać w swojej pamięci podręcznej. Musiałbym to odłożyć, ale to oznacza, że moja Mapa Tilemap jest w dwóch stanach, jednym, który nie może się odpowiednio oddać, i w pełni zbudowanym.