TL; DR Ho bisogno di aiuto per identificare le tecniche per semplificare i test di unità automatizzati quando lavoro all'interno di un framework stateful.
Sfondo:
Attualmente sto scrivendo un gioco in TypeScript e il framework Phaser . Phaser si descrive come un framework di gioco HTML5 che cerca il meno possibile di limitare la struttura del codice. Questo comporta alcuni compromessi, vale a dire che esiste un oggetto Phaser.Game che ti consente di accedere a tutto: cache, fisica, stati di gioco e altro ancora.
Questa statualità rende davvero difficile testare molte funzionalità, come il mio Tilemap. Vediamo un esempio:
Qui sto testando se i miei strati di piastrella sono corretti e posso identificare i muri e le creature all'interno del mio Tilemap:
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]
]);
Indipendentemente da ciò che faccio, non appena provo a creare la mappa, Phaser invoca internamente la sua cache, che viene popolata solo durante il runtime.
Non posso invocare questo test senza caricare l'intero gioco.
Una soluzione complessa potrebbe essere quella di scrivere un adattatore o un proxy che costruisca la mappa solo quando è necessario visualizzarla sullo schermo. Oppure potrei popolare il gioco caricando manualmente solo le risorse di cui ho bisogno e quindi utilizzandolo solo per la classe o il modulo di test specifici.
Ho scelto quella che ritengo sia una soluzione più pragmatica, ma estranea a questo. Tra il caricamento del mio gioco e il suo effettivo gioco, ho evitato un TestState
in che esegue il test con tutte le risorse e i dati memorizzati nella cache già caricati.
Questo è bello, perché posso testare tutte le funzionalità che desidero, ma anche sregolate, perché si tratta di un test di integrazione tecnico e ci si chiede se non potrei semplicemente guardare lo schermo e vedere se i nemici sono visualizzati. In realtà, no, potrebbero essere stati erroneamente identificati come un Articolo (già accaduto una volta) o, in seguito ai test, potrebbero non aver ricevuto eventi legati alla loro morte.
La mia domanda : lo shimming in uno stato di test come questo è comune? Esistono approcci migliori, soprattutto in ambiente JavaScript, di cui non sono a conoscenza?
Un altro esempio:
Ok, ecco un esempio più concreto per spiegare cosa sta succedendo:
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);
}
Costruisco il mio Tilemap da tre parti:
- La mappa è
key
- Il
manifest
dettaglio di tutte le risorse (fogli di piastrelle e fogli di calcolo) richiesti dalla mappa - A
mapDefinition
che descrive la struttura e i livelli della piastrella.
Innanzitutto, devo chiamare super per costruire il Tilemap all'interno di Phaser. Questa è la parte che invoca tutte quelle chiamate alla cache mentre tenta di cercare le risorse effettive e non solo le chiavi definite in manifest
.
In secondo luogo, associo i fogli di piastrelle e i livelli di piastrelle con il Tilemap. Ora può eseguire il rendering della mappa.
In terzo luogo, mi iterare attraverso i miei livelli e trovare tutti gli oggetti speciali che voglio estrusione dalla mappa: Creatures
, Items
, Interactables
e così via. Creo e memorizzo questi oggetti per un uso successivo.
Al momento ho ancora un'API relativamente semplice che mi consente di trovare, rimuovere, aggiornare queste entità:
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 });
}
È questa funzionalità che voglio controllare. Se non aggiungo i layer delle tessere o i set di tessere, la mappa non verrebbe visualizzata, ma potrei essere in grado di provarla. Tuttavia, anche chiamare super (...) invoca logiche specifiche del contesto o stateful che non riesco a isolare nei miei test.
new Tilemap(...)
Phaser inizia a scavare nella sua cache. Dovrei rimandarlo, ma ciò significa che il mio Tilemap è in due stati, uno che non può essere riprodotto correttamente e quello completamente costruito.