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.Respuestas:
Sin conocer Phaser o Typeccipt, todavía trato de darte una respuesta, porque los problemas que enfrentas son problemas que también son visibles con muchos otros marcos. El problema es que los componentes están estrechamente acoplados (todo apunta al objeto de Dios, y el objeto de Dios posee todo ...). Esto es algo que era poco probable que ocurriera si los creadores del framework crearan pruebas unitarias ellos mismos.
Básicamente tienes cuatro opciones:
Estas opciones no deben elegirse, a menos que todas las demás opciones fallen.
Elegir otro marco de trabajo que esté utilizando pruebas unitarias y pierda el acoplamiento hará que la vida sea mucho más fácil. Pero tal vez no haya ninguno que le guste y, por lo tanto, esté atrapado en el marco que tiene ahora. Escribir el tuyo puede llevar mucho tiempo.
Probablemente sea lo más fácil de hacer, pero realmente depende de cuánto tiempo tenga y qué tan dispuestos estén los creadores del marco a aceptar solicitudes de extracción.
Esta opción es probablemente la mejor opción para comenzar con las pruebas unitarias. Envuelva ciertos objetos que realmente necesita en las pruebas unitarias y cree objetos falsos para el resto.
fuente
Al igual que David, no estoy familiarizado con Phaser o Typecript, pero reconozco que sus preocupaciones son comunes a las pruebas unitarias con marcos y bibliotecas.
La respuesta corta es sí, shimming es la forma correcta y común de manejar esto con las pruebas unitarias . Creo que la desconexión es entender la diferencia entre las pruebas de unidades aisladas y las pruebas funcionales.
Las pruebas unitarias prueban que pequeñas secciones de su código producen resultados correctos. El objetivo de una prueba unitaria no incluye probar el código de terceros. La suposición es que el código ya ha sido probado para que funcione como lo espera el tercero. Al escribir una prueba unitaria para el código que se basa en un marco, es común calzar ciertas dependencias para preparar lo que parece un estado particular para el código, o calzar el marco / biblioteca por completo. Un ejemplo simple es la administración de sesiones para un sitio web: tal vez la laminilla siempre devuelve un estado válido y consistente en lugar de leer desde el almacenamiento. Otro ejemplo común es la eliminación de datos en la memoria y eludir cualquier biblioteca que consulte una base de datos, porque el objetivo no es probar la base de datos o la biblioteca que está utilizando para conectarse a ella, solo que su código procesa los datos correctamente.
Pero una buena prueba de unidad no significa que el usuario final verá exactamente lo que espera. Las pruebas funcionales toman más de una vista de alto nivel de que una característica completa está funcionando, marcos y todo Volviendo al ejemplo de un sitio web simple, una prueba funcional podría hacer una solicitud web a su código y verificar la respuesta para obtener resultados válidos. Se extiende por todo el código que se requiere para producir resultados. La prueba es para la funcionalidad más que para la corrección del código específico.
Así que creo que estás en el camino correcto con las pruebas unitarias. Para agregar pruebas funcionales de todo el sistema, crearía pruebas separadas que invoquen el tiempo de ejecución de Phaser y verifique los resultados.
fuente