Тестування одиничних можливостей, таких як Phaser?


9

TL; DR Мені потрібна допомога у виявленні методів для спрощення автоматизованого тестування одиниць при роботі в стані.


Фон:

В даний час я пишу гру в TypeScript та Phaser Framework . 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]
            ]);

Що б я не робив, як тільки я намагаюся створити карту, Фазер внутрішньо викликає кеш, який заповнюється лише під час виконання.

Я не можу викликати цей тест, не завантажуючи всю гру.

Складним рішенням може бути написання адаптера або проксі, який будує карту лише тоді, коли нам потрібно відобразити її на екрані. Або я міг би самостійно заповнити гру, завантаживши вручну лише потрібні вам активи, а потім використати її лише для певного тестового класу чи модуля.

Я вибрав те, що мені здається, більш прагматичним, але іноземним рішенням цього. Між завантаженням моєї гри та фактичною її грою я прошив 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 в.

По-перше, я повинен зателефонувати супер, щоб сконструювати Tilemap в Phaser. Це частина, яка викликає всі ці виклики до кешування, коли вона намагається шукати фактичні активи, а не лише ключі, визначені в manifest.

По-друге, я пов'язую плитки та шари плитки з 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 });
    }

Саме цю функціональність я хочу перевірити. Якщо я не додаю шари плитки або тілець, карта не відображатиметься, але я можу її перевірити. Однак навіть виклик супер (...) викликає логіку, залежну від контексту, або яку я не можу виділити у своїх тестах.


2
Я збентежений. Ви намагаєтеся перевірити, що Phaser виконує свою роботу з завантаження мапи черепиці, чи ви намагаєтесь перевірити вміст самої карти плитки? Якщо це колишній, ви, як правило, не перевіряєте, чи ваші залежності роблять свою роботу; це робота утримувача бібліотеки. Якщо останнє, ваша логічна гра занадто щільно поєднана з рамкою. Наскільки це дозволить продуктивність, ви хочете зберегти внутрішню роботу вашої гри чистою і залишити побічні ефекти на найвищих шарах програми, щоб уникнути подібного безладу.
Довал

Ні, я тестую власну функціональність. Вибачте, якщо тести не виглядають так, але трохи йде під прикриття. По суті, я переглядаю карту плитки і виявляю спеціальні плитки, які я перетворюю в ігрові об'єкти, такі як Предмети, Істоти тощо. Ця логіка є моєю і, безумовно, повинна бути перевірена.
IAE

1
Чи можете ви пояснити, як саме тоді Фазер бере участь у цьому? Мені не зрозуміло, куди потрапляє Фазер і чому. Звідки береться карта?
Doval

Вибачте за плутанину! Я додав код Tilemap як приклад одиниці функціональності, яку я намагаюся перевірити. Tilemap - це розширення (або, можливо, є -а) Phaser.Tilemap, що дозволяє мені надати таблицю карти з купою додаткової функціональності, яку я хотів би використовувати. Останній абзац підкреслює, чому я не можу перевірити його ізольовано. Навіть як компонент, в той момент, коли я просто new Tilemap(...)Фазер починає копати у своєму кеші. Мені доведеться відкласти це, але це означає, що мій Tilemap знаходиться у двох станах, в одному, який не може відображати себе належним чином, і повністю сконструйованому.
IAE

Мені здається, що, як я вже сказав у своєму першому коментарі, ваша логічна гра занадто поєднана з рамками. Ви повинні мати можливість запускати свою логіку гри, не вводячись у рамки. З'єднання карти плитки з активами, які використовуються для її малювання на екрані, стає на шляху.
Довал

Відповіді:


2

Не знаючи Phaser або Typescipt, я все ж намагаюся дати тобі відповідь, тому що проблеми, з якими ти стикаєшся, - це проблеми, які також видно з багатьма іншими рамками. Проблема полягає в тому, що компоненти повинні щільно з'єднуватися (усе вказує на об'єкт Бог, а об'єкт Бог володіє всім ...). Це те, що навряд чи відбудеться, якщо творці фреймворку самі створили блок-тести.

В основному у вас є чотири варіанти:

  1. Зупиніть тестування одиниць.
    Ці параметри не слід вибирати, якщо тільки всі інші параметри не вдається.
  2. Виберіть іншу рамку або напишіть свою.
    Вибір іншої основи, яка використовує тестування одиниць і втрачає зв'язок, значно полегшить життя. Але, можливо, немає жодної, яка вам сподобається, і тому ви застрягли в тій рамці, яку ви маєте зараз. Написання власного може зайняти багато часу.
  3. Внесіть свій внесок у рамки та зробіть тест дружнім.
    Напевно, це найпростіше зробити, але це дійсно залежить від того, скільки часу у вас є і наскільки готові творці фреймворку приймати запити на виклик.
  4. Обмотайте рамку.
    Цей варіант, мабуть, найкращий варіант, щоб почати тестування одиниць. Загорніть певні об'єкти, які вам справді потрібні, в одиниці тестів і створіть підроблені об’єкти для решти.

2

Як і Девід, я не знайомий з Phaser або Typescript, але я визнаю ваші занепокоєння спільними для тестування одиниць із рамками та бібліотеками.

Коротка відповідь - так, шиммінг - це правильний і поширений спосіб вирішити це за допомогою одиничного тестування . Я думаю, що відключення розуміє різницю між ізольованим модульним тестуванням та функціональним тестуванням.

Тестування блоку доводить, що невеликі ділянки вашого коду дають правильні результати. Мета одиничного тесту не включає тестування коду сторонніх розробників. Припущення полягає в тому, що код вже перевірений, щоб він працював так, як очікували треті сторони. Під час написання одиничного тесту для коду, який спирається на рамки, звичайно позбавляти певних залежностей, щоб підготувати те, що схоже на певний стан до коду, або загадати рамку / бібліотеку цілком. Простий приклад - управління сеансом для веб-сайту: можливо, shim завжди повертає дійсне, послідовне стан, а не читає зі сховища. Ще один поширений приклад - мерехтіння даних у пам’яті та обхід будь-якої бібліотеки, яка б запитувала базу даних, оскільки мета не тестувати базу даних або бібліотеку, яку ви використовуєте для підключення до неї, а лише те, що ваш код обробляє дані правильно.

Але гарне тестування одиниці не означає, що кінцевий користувач побачить саме те, що ви очікуєте. Функціональне тестування більше сприймає високий рівень того, що ціла функція працює, є рамки та все. Повернувшись до прикладу простого веб-сайту, функціональний тест може зробити веб-запит до вашого коду та перевірити відповідь на наявність дійсних результатів. Він охоплює весь код, необхідний для отримання результатів. Тест призначений для функціональності більше, ніж для конкретної коректності коду.

Тож я думаю, що ти на правильному шляху з тестуванням одиниць. Щоб додати функціональне тестування всієї системи, я створив би окремі тести, які викликають час виконання Phaser і перевіряють результати.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.