TL; DR Мне нужна помощь в определении методов для упрощения автоматического модульного тестирования при работе в среде с состоянием.
Фон:
В настоящее время я пишу игру на TypeScript и в инфраструктуре Phaser . Phaser описывает себя как игровую среду HTML5, которая старается как можно меньше ограничивать структуру вашего кода. Это имеет несколько компромиссов, а именно, что существует объект- бог Phaser.Game, который позволяет вам получить доступ ко всему: кешу, физике, игровым состояниям и многому другому .
Это состояние делает очень трудным тестирование многих функций, таких как мой Tilemap. Давайте посмотрим на пример:
Здесь я проверяю, правильно ли мои слои листов, и я могу идентифицировать стены и существа в моей 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]
]);
Независимо от того, что я делаю, как только я пытаюсь создать карту, Phaser внутренне вызывает ее кеш, который заполняется только во время выполнения.
Я не могу вызвать этот тест без загрузки всей игры.
Сложным решением может быть написание адаптера или прокси, который строит карту только тогда, когда нам нужно отобразить ее на экране. Или я мог бы сам заполнить игру, загрузив вручную только те ресурсы, которые мне нужны, а затем использовать ее только для определенного тестового класса или модуля.
Я выбрал то, что мне кажется более прагматичным, но иностранное решение для этого. Между загрузкой моей игры и фактической игрой я включил тест, TestState
который запускает тест со всеми активами и кэшированными данными, уже загруженными.
Это круто, потому что я могу протестировать все функциональные возможности, которые я хочу, но также и откатать, потому что это технический интеграционный тест, и возникает вопрос, не мог ли я просто посмотреть на экран и посмотреть, отображаются ли враги. На самом деле, нет, они могли быть ошибочно идентифицированы как Предмет (случился уже один раз), или - позже в тестах - им могли не дать события, связанные с их смертью.
Мой вопрос - это мерцание в тестовом состоянии, как это распространено? Есть ли лучшие подходы, особенно в среде JavaScript, о которых я не знаю?
Другой пример:
Хорошо, вот более конкретный пример, чтобы помочь объяснить, что происходит:
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);
}
Я строю свою Tilemap из трех частей:
- Карта
key
- В
manifest
детализирующем всех активы (tilesheets и spritesheets) требуемый карта - А,
mapDefinition
который описывает структуру и слои тайла карты.
Во-первых, я должен вызвать super для создания Tilemap внутри Phaser. Это та часть, которая вызывает все эти вызовы кеша при попытке поиска реальных ресурсов, а не только ключей, определенных в manifest
.
Во-вторых, я связываю листы листов и слои листов с картой листов. Теперь он может визуализировать карту.
В- третьих, я итерация через мои слои и найти какие - либо специальные объекты , которые я хочу , чтобы выдавливать из карты: Creatures
, Items
, Interactables
и так далее. Я создаю и храню эти объекты для последующего использования.
В настоящее время у меня все еще есть относительно простой API, который позволяет мне находить, удалять, обновлять эти объекты:
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 });
}
Именно эту функциональность я хочу проверить. Если я не добавлю слои листов или наборы плиток, карта не будет отображаться, но я мог бы проверить это. Тем не менее, даже вызов super (...) вызывает контекстно-зависимую логику или логику с состоянием, которую я не могу выделить в своих тестах.
new Tilemap(...)
Phaser начинает копаться в своем кеше. Я должен был бы отложить это, но это означает, что мой Tilemap находится в двух состояниях: одно, которое не может отображаться правильно, и полностью построенное.