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
который описывает структуру и слои тайла карты.
Во-первых, я должен вызвать super для создания Tilemap внутри Phaser. Это та часть, которая вызывает все эти вызовы кеша при попытке поиска реальных ресурсов, а не только ключей, определенных в 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 начинает копаться в своем кеше. Я должен был бы отложить это, но это означает, что мой Tilemap находится в двух состояниях: одно, которое не может отображаться правильно, и полностью построенное.Ответы:
Не зная Phaser или Typescipt, я все еще пытаюсь дать вам ответ, потому что проблемы, с которыми вы сталкиваетесь, - это проблемы, которые также видны во многих других средах. Проблема в том, что компоненты тесно связаны (все указывает на объект Бога, а объект Бога владеет всем ...). Это вряд ли произойдет, если создатели фреймворка сами создадут юнит-тесты.
В основном у вас есть четыре варианта:
Эти параметры не должны выбираться, если все остальные параметры не будут выполнены.
Выбор другого фреймворка, использующего модульное тестирование и потерявшего связь, значительно облегчит жизнь. Но, возможно, нет ни одного, который вам нравится, и поэтому вы застряли в рамках, которые у вас есть сейчас. Написание собственного может занять много времени.
Возможно, это проще всего сделать, но это действительно зависит от того, сколько у вас есть времени и насколько готовы создатели фреймворка принимать запросы на извлечение.
Этот вариант, вероятно, является лучшим вариантом для начала модульного тестирования. Оберните определенные объекты, которые вам действительно нужны в модульных тестах, и создайте поддельные объекты для остальных.
источник
Как и Дэвид, я не знаком с Phaser или Typescript, но я понимаю ваши опасения как общие для модульного тестирования с фреймворками и библиотеками.
Короткий ответ - да, шимминг - это правильный и распространенный способ справиться с этим с помощью модульного тестирования . Я думаю, что разъединение понимает разницу между изолированным модульным тестированием и функциональным тестированием.
Модульное тестирование доказывает, что небольшие разделы вашего кода дают правильные результаты. Цель юнит-теста не включает тестирование стороннего кода. Предполагается, что код уже протестирован и работает так, как ожидается третьей стороной. При написании модульного теста для кода, основанного на фреймворке, принято смешивать определенные зависимости, чтобы подготовить то, что выглядит как определенное состояние к коду, или полностью экранировать фреймворк / библиотеку. Простым примером является управление сессиями для веб-сайта: возможно, оболочка всегда возвращает действительное согласованное состояние вместо чтения из хранилища. Другим распространенным примером является отбрасывание данных в памяти и обход любой библиотеки, которая будет запрашивать базу данных, поскольку цель состоит не в том, чтобы протестировать базу данных или библиотеку, которую вы используете для подключения к ней, а в том, что ваш код обрабатывает данные правильно.
Но хорошее модульное тестирование не означает, что конечный пользователь увидит именно то, что вы ожидаете. Функциональное тестирование предполагает более высокое представление о том, что работает целая функция, фреймворки и все. Возвращаясь к примеру простого веб-сайта, функциональный тест может сделать веб-запрос к вашему коду и проверить ответ на наличие действительных результатов. Он охватывает весь код, необходимый для получения результатов. Тест на функциональность больше, чем на конкретную правильность кода.
Так что я думаю, что вы на правильном пути с модульным тестированием. Чтобы добавить функциональное тестирование всей системы, я бы создал отдельные тесты, которые запускают среду выполнения Phaser и проверяют результаты.
источник