Unit Testing kerangka kerja stateful seperti Phaser?

9

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 TestStateyang 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 manifestmerinci seluruh aset (tilesheets dan spritesheets) yang dibutuhkan oleh peta
  • A mapDefinitionyang 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, Interactablesdan 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.

IAE
sumber
2
Saya bingung. Apakah Anda mencoba menguji apakah Phaser melakukan tugasnya memuat tilemap atau Anda mencoba menguji konten tilemap itu sendiri? Jika yang pertama, Anda biasanya tidak menguji bahwa dependensi Anda melakukan pekerjaan mereka; itulah tugas pengelola perpustakaan. Jika yang terakhir, logika gim Anda terlalu erat dengan kerangka. Sebesar apa pun kinerja yang memungkinkan, Anda ingin menjaga kinerja batin game Anda tetap murni dan meninggalkan efek samping ke lapisan paling atas program untuk menghindari kekacauan semacam ini.
Doval
Tidak, saya sedang menguji fungsionalitas saya sendiri. Maaf jika tesnya tidak seperti itu, tapi ada sedikit masalah. Pada dasarnya, saya melihat melalui tilemap dan menemukan ubin khusus yang saya ubah menjadi entitas game seperti Item, Creatures, dan sebagainya. Logika ini milikku dan harus diuji.
IAE
1
Bisakah Anda jelaskan bagaimana sebenarnya Phaser terlibat dalam hal ini? Tidak jelas bagi saya di mana Phaser dipanggil dan mengapa. Dari mana peta itu berasal?
Doval
Saya minta maaf atas kebingungannya! Saya telah menambahkan kode Tilemap saya sebagai contoh unit fungsionalitas yang saya coba uji. Tilemap adalah ekstensi (atau opsional memiliki-a) Phaser.Tilemap yang memungkinkan saya membuat tilemap dengan banyak fungsi tambahan yang ingin saya gunakan. Paragraf terakhir menyoroti mengapa saya tidak bisa mengujinya secara terpisah. Bahkan sebagai komponen, saat saya baru saja 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.
IAE
Sepertinya saya, seperti yang saya katakan di komentar pertama saya, logika permainan Anda terlalu digabungkan ke kerangka kerja. Anda harus dapat menjalankan logika game Anda tanpa membawa kerangka kerja sama sekali. Menggabungkan peta ubin ke aset yang digunakan untuk menggambarnya di layar semakin menghalangi.
Doval

Jawaban:

2

Tidak mengenal Phaser atau Typcipt, saya masih mencoba memberi Anda jawaban, karena masalah yang Anda hadapi adalah masalah yang juga terlihat dengan banyak kerangka kerja lainnya. Masalahnya adalah bahwa komponen harus digabungkan dengan erat (semuanya menunjuk ke objek Dewa, dan objek Dewa memiliki segalanya ...). Ini adalah sesuatu yang tidak mungkin terjadi jika pembuat kerangka menciptakan unit-test sendiri.

Pada dasarnya Anda memiliki empat opsi:

  1. Hentikan pengujian unit.
    Opsi ini tidak boleh dipilih, kecuali semua opsi lain gagal.
  2. Pilih kerangka kerja lain atau tulis sendiri.
    Memilih kerangka kerja lain yang menggunakan unit-testing dan telah kehilangan kopling, akan membuat hidup jadi lebih mudah. Tapi mungkin tidak ada yang Anda sukai dan karenanya Anda terjebak dengan kerangka yang Anda miliki sekarang. Menulis sendiri bisa memakan banyak waktu.
  3. Berkontribusi pada kerangka kerja dan membuatnya ramah uji.
    Mungkin yang termudah untuk dilakukan, tetapi itu benar-benar tergantung pada berapa banyak waktu yang Anda miliki dan seberapa bersedia pembuat kerangka untuk menerima permintaan tarik.
  4. Bungkus kerangka kerja.
    Opsi ini mungkin merupakan opsi terbaik untuk memulai pengujian unit. Bungkus benda-benda tertentu yang benar-benar Anda butuhkan dalam unit-tes dan buat benda-benda palsu untuk sisanya.
David Perfors
sumber
2

Seperti David, saya tidak akrab dengan Phaser atau Script, tapi saya menyadari kekhawatiran Anda sebagai hal yang umum untuk pengujian unit dengan kerangka kerja dan perpustakaan.

Jawaban singkatnya adalah ya, shimming adalah cara yang benar dan umum untuk menangani ini dengan pengujian unit . Saya pikir putuskan memahami perbedaan antara pengujian unit terisolasi dan pengujian fungsional.

Pengujian unit membuktikan bahwa bagian kecil dari kode Anda menghasilkan hasil yang benar. Tujuan dari pengujian unit tidak termasuk pengujian kode pihak ke-3. Asumsinya adalah bahwa kode tersebut sudah diuji untuk berfungsi seperti yang diharapkan oleh pihak ke-3. Saat menulis unit test untuk kode yang bergantung pada suatu kerangka kerja, biasanya shim dependensi tertentu untuk mempersiapkan apa yang tampak seperti keadaan tertentu pada kode, atau untuk shim kerangka / pustaka seluruhnya. Contoh sederhana adalah manajemen sesi untuk situs web: mungkin shim selalu mengembalikan keadaan yang valid dan konsisten alih-alih membaca dari penyimpanan. Contoh umum lainnya adalah memotong data dalam memori dan mem-bypass pustaka yang akan meminta basis data, karena tujuannya bukan untuk menguji pangkalan data atau pustaka yang Anda gunakan untuk terhubung dengannya, hanya saja kode Anda memproses data dengan benar.

Tetapi pengujian unit yang baik tidak berarti pengguna akhir akan melihat persis apa yang Anda harapkan. Pengujian fungsional membutuhkan lebih banyak pandangan tingkat tinggi bahwa seluruh fitur berfungsi, kerangka kerja dan semuanya. Kembali ke contoh situs web sederhana, tes fungsional mungkin membuat permintaan web ke kode Anda dan memeriksa respons untuk hasil yang valid. Itu mencakup semua kode yang diperlukan untuk menghasilkan hasil. Tes ini untuk fungsionalitas lebih dari untuk kebenaran kode tertentu.

Jadi saya pikir Anda berada di jalur yang benar dengan pengujian unit. Untuk menambahkan pengujian fungsional seluruh sistem saya akan membuat tes terpisah yang menjalankan runtime Phaser dan memeriksa hasilnya.

Matt S
sumber