TL; DR Preciso de ajuda na identificação de técnicas para simplificar o teste de unidade automatizado ao trabalhar em uma estrutura estável.
Fundo:
Atualmente, estou escrevendo um jogo no TypeScript e na estrutura da Phaser . A Phaser se descreve como uma estrutura de jogo HTML5 que tenta o mínimo possível para restringir a estrutura do seu código. Isso vem com algumas trocas, a saber, que existe um objeto de Deus Phaser.Game que permite acessar tudo: cache, física, estados do jogo e muito mais.
Esse estado torna muito difícil testar muitas funcionalidades, como o meu Tilemap. Vamos ver um exemplo:
Aqui estou testando se minhas camadas de mosaico estão ou não corretamente e posso identificar as paredes e criaturas no meu 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]
]);
Não importa o que eu faça, assim que tento criar o mapa, a Phaser chama internamente seu cache, que é preenchido apenas durante o tempo de execução.
Não posso invocar esse teste sem carregar o jogo inteiro.
Uma solução complexa pode ser escrever um adaptador ou proxy que apenas construa o mapa quando precisarmos exibi-lo na tela. Ou eu mesmo poderia preencher o jogo carregando manualmente apenas os ativos necessários e, em seguida, usando-o apenas para a classe ou módulo de teste específico.
Eu escolhi o que considero uma solução mais pragmática, mas estranha para isso. Entre o carregamento do meu jogo e a execução real, eu calcei um TestState
que executa o teste com todos os ativos e dados em cache já carregados.
Isso é legal, porque eu posso testar todas as funcionalidades que eu quero, mas também não é legal, porque esse é um teste técnico de integração e nos perguntamos se eu não poderia apenas olhar para a tela e ver se os inimigos são exibidos. Na verdade, não, eles podem ter sido identificados erroneamente como um Item (já aconteceu uma vez) ou - posteriormente nos testes - eles podem não ter recebido eventos relacionados à sua morte.
A minha pergunta - é comum o shimming em um estado de teste como este? Existem abordagens melhores, especialmente no ambiente JavaScript, das quais não conheço?
Outro exemplo:
Ok, aqui está um exemplo mais concreto para ajudar a explicar o que está acontecendo:
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);
}
Eu construo meu Tilemap a partir de três partes:
- Os mapas
key
- O
manifest
detalhamento de todos os ativos (planilhas e sprites) exigidos pelo mapa - A
mapDefinition
que descreve a estrutura e as camadas do tilemap.
Primeiro, devo chamar super para construir o Tilemap na Phaser. Essa é a parte que chama todas essas chamadas para o cache, enquanto tenta procurar os ativos reais e não apenas as chaves definidas no manifest
.
Segundo, associo as folhas de mosaico e as camadas de mosaico ao Tilemap. Agora ele pode renderizar o mapa.
Terceiro, eu iterate através das minhas camadas e encontrar objetos especiais que eu quero extrusão a partir do mapa: Creatures
, Items
, Interactables
e assim por diante. Eu crio e armazeno esses objetos para uso posterior.
Atualmente, ainda tenho uma API relativamente simples que permite encontrar, remover, atualizar essas entidades:
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 });
}
É essa funcionalidade que quero verificar. Se eu não adicionar as camadas ou os conjuntos de blocos, o mapa não será renderizado, mas talvez eu possa testá-lo. No entanto, mesmo chamar super (...) invoca uma lógica de estado ou de estado específico que não consigo isolar em meus testes.
new Tilemap(...)
Phaser começa a cavar em seu cache. Eu teria que adiar isso, mas isso significa que meu Tilemap está em dois estados, um que não pode render-se adequadamente e o totalmente construído.Respostas:
Não conhecendo Phaser ou Typescipt, ainda tento dar uma resposta, porque os problemas que você está enfrentando são também visíveis em muitas outras estruturas. O problema é que os componentes estão fortemente acoplados (tudo aponta para o objeto de Deus, e o objeto de Deus é dono de tudo ...). É improvável que isso aconteça se os criadores da estrutura criarem os próprios testes de unidade.
Basicamente, você tem quatro opções:
Esta opção não deve ser escolhida, a menos que todas as outras opções falhem.
A escolha de outra estrutura que esteja usando o teste de unidade e com perda de acoplamento tornará a vida muito mais fácil. Mas talvez não haja nada que você goste e, portanto, você está preso à estrutura que possui agora. Escrever o seu próprio pode levar muito tempo.
Provavelmente o mais fácil de fazer, mas realmente depende de quanto tempo você tem e de como os criadores da estrutura estão dispostos a aceitar solicitações pull.
Esta opção é provavelmente a melhor opção para iniciar o teste de unidade. Enrole certos objetos que você realmente precisa nos testes de unidade e crie objetos falsos para o resto.
fonte
Como David, não estou familiarizado com Phaser ou Typescript, mas reconheço suas preocupações como comuns ao teste de unidade com estruturas e bibliotecas.
A resposta curta é sim, shimming é a maneira correta e comum de lidar com isso com testes de unidade . Eu acho que a desconexão está entendendo a diferença entre testes de unidade isolados e testes funcionais.
O teste de unidade prova que pequenas seções do seu código produzem resultados corretos. O objetivo de um teste de unidade não inclui o teste de código de terceiros. A suposição é que o código já foi testado para funcionar como esperado pela terceira parte. Ao escrever um teste de unidade para código que depende de uma estrutura, é comum calçar certas dependências para preparar o código que parece um estado específico para o código ou calçar a estrutura / biblioteca inteiramente. Um exemplo simples é o gerenciamento de sessões de um site: talvez o shim sempre retorne um estado válido e consistente, em vez de ler do armazenamento. Outro exemplo comum é reduzir os dados na memória e ignorar qualquer biblioteca que consulta um banco de dados, porque o objetivo não é testar o banco de dados ou a biblioteca que você está usando para se conectar a ele, apenas que o seu código processa os dados corretamente.
Mas um bom teste de unidade não significa que o usuário final verá exatamente o que você espera. O teste funcional tem uma visão de alto nível de que todo um recurso está funcionando, estruturas e tudo. De volta ao exemplo de um site simples, um teste funcional pode fazer uma solicitação da Web para o seu código e verificar a resposta para obter resultados válidos. Ele abrange todo o código necessário para produzir resultados. O teste é mais para funcionalidade do que para correção de código específico.
Então, acho que você está no caminho certo com o teste de unidade. Para adicionar testes funcionais de todo o sistema, eu criaria testes separados que invocariam o tempo de execução da Phaser e verificariam os resultados.
fonte