TL; DR J'ai besoin d'aide pour identifier les techniques permettant de simplifier les tests unitaires automatisés lorsque je travaille dans un cadre dynamique.
Contexte:
J'écris actuellement un jeu en TypeScript et le framework Phaser . Phaser se décrit comme un framework de jeu HTML5 qui essaie le moins possible de restreindre la structure de votre code. Cela vient avec quelques compromis, à savoir qu'il existe un Dieu-objet Phaser.Game qui vous permet d'accéder à tout: le cache, la physique, les états de jeu, etc.
Cet état, il est vraiment difficile de tester de nombreuses fonctionnalités, telles que mon Tilemap. Voyons un exemple:
Ici, je teste si mes couches de tuiles sont correctes et je peux identifier les murs et les créatures dans mon 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]
]);
Peu importe ce que je fais, dès que j'essaie de créer la carte, Phaser invoque en interne son cache, qui n'est rempli que pendant l'exécution.
Je ne peux pas invoquer ce test sans charger l'intégralité du jeu.
Une solution complexe pourrait être d'écrire un adaptateur ou un proxy qui ne construit la carte que lorsque nous devons l'afficher à l'écran. Ou je pourrais remplir le jeu moi-même en chargeant manuellement uniquement les ressources dont j'ai besoin, puis en l'utilisant uniquement pour la classe ou le module de test spécifique.
J'ai choisi ce que j'estime être une solution plus pragmatique mais étrangère à cela. Entre le chargement de mon jeu et la lecture réelle de celui-ci, j'ai calé un TestState
qui exécute le test avec tous les actifs et les données en cache déjà chargés.
C'est cool, car je peux tester toutes les fonctionnalités que je veux, mais aussi pas cool, car c'est un test d'intégration technique et on se demande si je ne pourrais pas simplement regarder l'écran et voir si les ennemis sont affichés. En fait, non, ils pourraient avoir été mal identifiés comme un élément (déjà arrivé une fois) ou - plus tard dans les tests - ils pourraient ne pas avoir reçu d'événements liés à leur mort.
Ma question - Le calage dans un état de test comme celui-ci est-il courant? Y a-t-il de meilleures approches, en particulier dans l'environnement JavaScript, que je ne connais pas?
Un autre exemple:
D'accord, voici un exemple plus concret pour aider à expliquer ce qui se passe:
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);
}
Je construis mon Tilemap à partir de trois parties:
- Les cartes
key
- Le
manifest
détail de tous les actifs (feuilles de tuiles et feuilles de sprites) requis par la carte - A
mapDefinition
qui décrit la structure et les couches du tilemap.
Tout d'abord, je dois appeler super pour construire le Tilemap dans Phaser. C'est la partie qui appelle tous ces appels à mettre en cache pendant qu'elle essaie de rechercher les actifs réels et pas seulement les clés définies dans le manifest
.
Deuxièmement, j'associe les feuilles et les couches de tuiles au Tilemap. Il peut maintenant rendre la carte.
Troisièmement, je itérer à travers mes couches et trouver des objets spéciaux que je veux expulsent de la carte: Creatures
, Items
, Interactables
et ainsi de suite. Je crée et stocke ces objets pour une utilisation ultérieure.
J'ai actuellement encore une API relativement simple qui me permet de trouver, supprimer, mettre à jour ces entités:
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 });
}
C'est cette fonctionnalité que je veux vérifier. Si je n'ajoute pas les couches de tuiles ou les ensembles de tuiles, la carte ne sera pas rendue, mais je pourrai peut-être la tester. Cependant, même appeler super (...) invoque une logique contextuelle ou avec état que je ne peux pas isoler dans mes tests.
new Tilemap(...)
Phaser commence à creuser dans son cache. Je devrais reporter cela, mais cela signifie que mon Tilemap est dans deux états, celui qui ne peut pas se rendre correctement et celui entièrement construit.