TL; DR Ho bisogno di aiuto per identificare le tecniche per semplificare i test unitari automatizzati quando si lavora 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. Ciò comporta alcuni compromessi, vale a dire che esiste un God-object Phaser.Game che ti consente di accedere tutto: cache, fisica, stati del gioco e altro.
Questo stato di cose rende molto difficile testare molte funzionalità, come la mia Tilemap. Vediamo un esempio:
Qui sto verificando se i miei riquadri sono o meno corretti e posso identificare i muri e le creature nella mia 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]
]);
Non importa quello 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 scrivere un adattatore o un proxy che costruisce la mappa solo quando è necessario visualizzarlo sullo schermo. Oppure potrei compilare il gioco manualmente caricando solo le risorse di cui ho bisogno e quindi utilizzandolo solo per la specifica classe di test o modulo.
Ho scelto ciò che ritengo sia una soluzione più pragmatica, ma estranea a questo. Tra il caricamento del mio gioco e la sua esecuzione effettiva, ho eseguito il rendering di un TestState
in cui viene eseguito il test con tutte le risorse e i dati memorizzati nella cache già caricati.
Questo è bello, perché posso testare tutte le funzionalità che voglio, ma anche uncool, perché questo è un test di integrazione tecnica 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 Item (successo già una volta) o, più tardi nei test, non avrebbero potuto ricevere eventi legati alla loro morte.
La mia domanda - Lo shimming in uno stato di test come questo comune? Ci sono approcci migliori, specialmente nell'ambiente JavaScript, di cui non sono a conoscenza?
Un altro esempio:
Ok, ecco un esempio più concreto per aiutare a 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 la mia Tilemap da tre parti:
- La mappa
key
- Il
manifest
che riporta in dettaglio tutte le risorse (fogli di calcolo e fogli di calcolo) richiesti dalla mappa - Un
mapDefinition
che descrive la struttura e i livelli della tilemap.
Per prima cosa, devo chiamare super per costruire la Tilemap all'interno di Phaser. Questa è la parte che richiama tutte quelle chiamate alla cache mentre prova a cercare le risorse effettive e non solo le chiavi definite in manifest
.
In secondo luogo, associo i fogli di piastrelle e i livelli di tessere con la Tilemap. Ora può rendere la mappa.
In terzo luogo, faccio scorrere i miei strati e trovo gli oggetti speciali che voglio estrudere dalla mappa: Creatures
, Items
, Interactables
e così via. Creo e memorizzo questi oggetti per un uso futuro.
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 Livelli Tile o Tileset, la mappa non verrà visualizzata, ma potrei riuscire a verificarla. Tuttavia, anche chiamando super (...) invoca una logica specifica del contesto o di stato che non riesco a isolare nei miei test.