TL; DR Ich benötige Hilfe bei der Identifizierung von Techniken zur Vereinfachung des automatisierten Komponententests, wenn ich in einem zustandsbehafteten Rahmen arbeite.
Hintergrund:
Ich schreibe gerade ein Spiel in TypeScript und dem Phaser-Framework . Phaser beschreibt sich selbst als ein HTML5-Spielframework, das so wenig wie möglich versucht, die Struktur Ihres Codes einzuschränken. Dies bringt einige Nachteile mit sich , nämlich dass es ein Gott-Objekt Phaser.Game gibt, mit dem Sie auf alles zugreifen können: den Cache, die Physik, die Spielzustände und vieles mehr.
Diese Statefulness macht es wirklich schwierig, viele Funktionen wie meine Tilemap zu testen. Sehen wir uns ein Beispiel an:
Hier teste ich, ob meine Kachelschichten korrekt sind oder nicht, und kann die Wände und Kreaturen in meiner Tilemap identifizieren:
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]
]);
Egal was ich mache, sobald ich versuche, die Karte zu erstellen, ruft Phaser intern den Cache auf, der nur zur Laufzeit gefüllt wird.
Ich kann diesen Test nicht aufrufen, ohne das gesamte Spiel zu laden.
Eine komplexe Lösung könnte darin bestehen, einen Adapter oder Proxy zu schreiben, der die Karte nur erstellt, wenn sie auf dem Bildschirm angezeigt werden muss. Oder ich könnte das Spiel selbst füllen, indem ich nur die benötigten Assets manuell lade und es dann nur für die bestimmte Testklasse oder das jeweilige Modul verwende.
Ich entschied mich für eine meiner Meinung nach pragmatischere, aber fremde Lösung. Zwischen dem Laden meines Spiels und dem tatsächlichen Spielen habe ich ein TestState
In eingeblendet , das den Test mit allen bereits geladenen Assets und zwischengespeicherten Daten ausführt .
Das ist cool, weil ich alle gewünschten Funktionen testen kann, aber auch uncool, weil dies ein technischer Integrationstest ist und man sich fragt, ob ich nicht einfach auf den Bildschirm schauen und sehen kann, ob die Feinde angezeigt werden. Nein, sie wurden möglicherweise fälschlicherweise als Gegenstand identifiziert (ist bereits einmal aufgetreten), oder - später in den Tests - haben sie möglicherweise keine Ereignisse erhalten, die mit ihrem Tod verbunden sind.
Meine Frage - Ist Shimming in einem Testzustand wie diesem üblich? Gibt es bessere Ansätze, insbesondere in der JavaScript-Umgebung, die mir nicht bekannt sind?
Ein anderes Beispiel:
Okay, hier ist ein konkreteres Beispiel, um zu erklären, was passiert:
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);
}
Ich konstruiere meine Tilemap aus drei Teilen:
- Die Karten
key
- Die
manifest
Detaillierung aller Assets (Kacheln und Spritesheets), die von der Karte benötigt werden - A
mapDefinition
, das die Struktur und die Ebenen der Tilemap beschreibt.
Zuerst muss ich super aufrufen, um die Tilemap in Phaser zu erstellen. Dies ist der Teil, der alle diese Aufrufe zum Cache aufruft, wenn versucht wird, die tatsächlichen Assets und nicht nur die in der Liste definierten Schlüssel nachzuschlagen manifest
.
Zweitens ordne ich die Kachelblätter und Kachelebenen der Tilemap zu. Es kann jetzt die Karte rendern.
Drittens, ich Iterierte durch meine Schichten und findet spezielle Objekte , die ich von der Karte zu extrudieren will: Creatures
, Items
, Interactables
und so weiter. Ich erstelle und speichere diese Objekte für die spätere Verwendung.
Ich habe derzeit noch eine relativ einfache API, mit der ich diese Entitäten finden, entfernen und aktualisieren kann:
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 });
}
Diese Funktionalität möchte ich überprüfen. Wenn ich die Kachelebenen oder Kachelsätze nicht hinzufüge, wird die Karte nicht gerendert, aber ich kann sie möglicherweise testen. Selbst wenn Sie super (...) aufrufen, wird eine kontextspezifische oder zustandsbehaftete Logik aufgerufen, die ich in meinen Tests nicht isolieren kann.
new Tilemap(...)
Phaser anfange, in seinem Cache zu graben. Ich müsste das verschieben, aber das bedeutet, dass sich meine Tilemap in zwei Zuständen befindet, einem, der sich nicht richtig rendern kann, und dem vollständig erstellten.