TL; DR Saya perlu bantuan dalam mengidentifikasi teknik untuk menyederhanakan pengujian unit otomatis ketika bekerja dalam kerangka stateful.
Latar Belakang:
Saat ini saya sedang menulis game dalam kerangka kerja TypeScript dan Phaser . Phaser menggambarkan dirinya sebagai kerangka kerja permainan HTML5 yang mencoba sesedikit mungkin untuk membatasi struktur kode Anda. Ini datang dengan beberapa trade-off, yaitu ada Phaser-objek Dewa. Game yang memungkinkan Anda mengakses semuanya: cache, fisika, status permainan, dan banyak lagi.
Keadaan ini membuatnya sangat sulit untuk menguji banyak fungsi, seperti Tilemap saya. Mari kita lihat sebuah contoh:
Di sini saya menguji apakah lapisan ubin saya benar atau tidak dan saya dapat mengidentifikasi dinding dan makhluk dalam Tilemap saya:
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]
]);
Tidak peduli apa yang saya lakukan, segera setelah saya mencoba membuat peta, Phaser secara internal memanggil cache itu, yang hanya diisi selama runtime.
Saya tidak bisa menjalankan tes ini tanpa memuat seluruh game.
Solusi kompleks mungkin dengan menulis Adaptor atau Proksi yang hanya membangun peta ketika kita perlu menampilkannya di layar. Atau saya bisa mengisi permainan sendiri dengan memuat secara manual hanya aset yang saya butuhkan dan kemudian menggunakannya hanya untuk kelas tes atau modul tertentu.
Saya memilih apa yang saya rasa lebih pragmatis, tetapi solusi asing untuk ini. Antara memuat game saya dan bermain yang sebenarnya, saya shimmed a TestState
yang menjalankan tes dengan semua aset dan data cache sudah dimuat.
Ini keren, karena saya dapat menguji semua fungsi yang saya inginkan, tetapi juga tidak keren, karena ini adalah tes integrasi teknis dan orang bertanya-tanya apakah saya tidak bisa hanya melihat layar dan melihat apakah musuh ditampilkan. Sebenarnya, tidak, mereka mungkin telah salah diidentifikasi sebagai suatu Item (terjadi sekali saja) atau - kemudian dalam tes - mereka mungkin tidak diberi peristiwa yang terkait dengan kematian mereka.
Pertanyaan saya - Apakah mencukur dalam kondisi uji seperti ini biasa? Apakah ada pendekatan yang lebih baik, terutama di lingkungan JavaScript, yang tidak saya sadari?
Contoh lain:
Oke, inilah contoh yang lebih konkret untuk membantu menjelaskan apa yang terjadi:
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);
}
Saya membuat Tilemap saya dari tiga bagian:
- Peta itu
key
- The
manifest
merinci seluruh aset (tilesheets dan spritesheets) yang dibutuhkan oleh peta - A
mapDefinition
yang menggambarkan struktur dan lapisan tilemap.
Pertama, saya harus memanggil super untuk membangun Tilemap dalam Phaser. Ini adalah bagian yang memanggil semua panggilan ke cache karena mencoba mencari aset yang sebenarnya dan bukan hanya kunci yang ditentukan dalam manifest
.
Kedua, saya mengasosiasikan tilesheets dan layer tile dengan Tilemap. Sekarang dapat merender peta.
Ketiga, saya iterate melalui lapisan saya dan menemukan benda-benda khusus yang ingin saya mengusir dari peta: Creatures
, Items
, Interactables
dan sebagainya. Saya membuat dan menyimpan objek-objek ini untuk digunakan nanti.
Saat ini saya masih memiliki API yang relatif sederhana yang memungkinkan saya menemukan, menghapus, memperbarui entitas ini:
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 });
}
Ini fungsi yang ingin saya periksa. Jika saya tidak menambahkan Layers Tiles atau Tilesets, peta tidak akan merender, tapi saya mungkin bisa mengujinya. Namun, bahkan memanggil super (...) memanggil logika konteks-spesifik atau stateful yang saya tidak dapat mengisolasi dalam tes saya.
new Tilemap(...)
Phaser mulai menggali cache-nya. Saya harus menunda itu, tetapi itu berarti Tilemap saya ada di dua negara, yang tidak dapat membuat dirinya dengan benar, dan yang sepenuhnya dibangun.