TL; DRステートフルなフレームワーク内で作業する場合、自動化された単体テストを簡略化するための手法を特定するのに役立ちます。
バックグラウンド:
TypeScriptとPhaserフレームワークでゲームを書いています。Phaserは、コードの構造をできるだけ制限しないHTML5ゲームフレームワークとしての地位を占めています。これにはいくつかのトレードオフが伴います。つまり、キャッシュ、物理、ゲームの状態など、すべてにアクセスできるGod-object Phaser.Gameが存在します。
このステートフル性は、私のタイルマップなどの多くの機能をテストすることを本当に困難にします。例を見てみましょう:
ここでは、タイルレイヤーを正しくテストして、タイルマップ内の壁と生き物を識別できるかどうかをテストしています。
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
すべてのアセットとキャッシュデータが既に読み込まれた状態でテストを実行するin をシムしました。
必要なすべての機能をテストできるのでこれはクールですが、これは技術的な統合テストであり、画面を見て敵が表示されているかどうかを確認できないのではないかと不思議に思っています。実際、いいえ、彼らはアイテムとして誤認された可能性があります(既に一度起こった)またはテストの後半で、彼らの死に関連するイベントが与えられなかった可能性があります。
私の質問 -このようなテスト状態でシミングは一般的ですか?特に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);
}
私は3つの部分からタイルマップを作成します。
- 地図の
key
manifest
マップに必要なすべてのアセット(タイルシートとスプライトシート)の詳細- A
mapDefinition
タイルマップの構造や層が記載されています。
まず、スーパーを呼び出してPhaser内でTilemapを構築する必要があります。これは、で定義されたキーだけでなく、実際のアセットを検索しようとするときに、キャッシュへのすべての呼び出しを呼び出す部分です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がキャッシュを掘り始めた瞬間です。私はそれを延期する必要がありますが、それは私のタイルマップが2つの状態にあることを意味します。1つは適切にレンダリングできない状態、もう1つは完全に構築された状態です。