TL; DR Necesito ayuda para identificar técnicas para simplificar las pruebas unitarias automatizadas cuando trabajo dentro de un marco con estado.
Antecedentes:
Actualmente estoy escribiendo un juego en TypeScript y el marco Phaser . Phaser se describe a sí mismo como un marco de juego HTML5 que intenta lo menos posible para restringir la estructura de su código. Esto viene con algunas compensaciones, a saber, que existe un Phaser.Game de God-object que le permite acceder a todo: el caché, la física, los estados del juego y más.
Esta capacidad de estado hace que sea realmente difícil probar muchas funciones, como mi Tilemap. Veamos un ejemplo:
Aquí estoy probando si mis capas de mosaico son correctas o no y puedo identificar las paredes y las criaturas dentro de mi mapa de mosaico:
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]
]);
No importa lo que haga, tan pronto como intento crear el mapa, Phaser invoca internamente su caché, que solo se completa durante el tiempo de ejecución.
No puedo invocar esta prueba sin cargar todo el juego.
Una solución compleja podría ser escribir un Adaptador o Proxy que solo construya el mapa cuando necesitemos mostrarlo en la pantalla. O podría completar el juego yo mismo cargando manualmente solo los recursos que necesito y luego usándolo solo para la clase o módulo de prueba específico.
Elegí lo que siento es una solución más pragmática, pero extraña para esto. Entre la carga de mi juego y la reproducción real del juego, introduje una TestState
prueba que ejecuta la prueba con todos los activos y datos en caché ya cargados.
Esto es genial, porque puedo probar toda la funcionalidad que quiero, pero también no es genial, porque esta es una prueba técnica de integración y uno se pregunta si no podría simplemente mirar la pantalla y ver si se muestran los enemigos. En realidad, no, podrían haber sido identificados erróneamente como un artículo (ya sucedió una vez) o, más adelante en las pruebas, podrían no haber recibido eventos relacionados con su muerte.
Mi pregunta : ¿es común el shimming en un estado de prueba como este? ¿Hay mejores enfoques, especialmente en el entorno de JavaScript, que no conozco?
Otro ejemplo:
Bien, aquí hay un ejemplo más concreto para ayudar a explicar lo que está sucediendo:
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);
}
Construyo mi Tilemap a partir de tres partes:
- Los mapas
key
- El
manifest
detalle de todos los activos (hojas de mosaico y hojas de sprites) requeridos por el mapa - A
mapDefinition
que describe la estructura y las capas del mosaico.
Primero, debo llamar a super para construir el Tilemap dentro de Phaser. Esta es la parte que invoca todas esas llamadas a la memoria caché al intentar buscar los activos reales y no solo las claves definidas en manifest
.
En segundo lugar, asocio las hojas de mosaico y las capas de mosaico con el mapa de mosaico. Ahora puede representar el mapa.
En tercer lugar, iterar a través de mis capas y encontrar todos los objetos especiales que quiero extrusión del mapa: Creatures
, Items
, Interactables
y así sucesivamente. Creo y almaceno estos objetos para su uso posterior.
Actualmente todavía tengo una API relativamente simple que me permite encontrar, eliminar y actualizar estas entidades:
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 });
}
Es esta funcionalidad la que quiero verificar. Si no agrego las capas de mosaico o los conjuntos de mosaico, el mapa no se representará, pero podría probarlo. Sin embargo, incluso llamar a super (...) invoca una lógica específica de contexto o de estado que no puedo aislar en mis pruebas.
new Tilemap(...)
Phaser comienza a cavar en su caché. Tendría que diferir eso, pero eso significa que mi Tilemap está en dos estados, uno que no se puede representar correctamente y el completamente construido.