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
描述tilemap的的结构和层次。
首先,我必须调用super以在Phaser中构造Tilemap。这是在尝试查找实际资产而不是仅在中定义的键时调用所有这些缓存调用的部分manifest
。
其次,我将tilesheets和tile图层与Tilemap关联。现在可以渲染地图了。
第三,我迭代通过我的层和发现任何特殊对象,我想从地图挤压: 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处于两种状态,一种状态无法正确呈现自身,另一种是完全构建的状态。