TL; DR Potrzebuję pomocy w identyfikacji technik upraszczających zautomatyzowane testowanie jednostek podczas pracy w ramach stanowych.
Tło:
Obecnie piszę grę w TypeScript i frameworku Phaser . Phaser opisuje się jako środowisko gry HTML5, które stara się jak najmniej ograniczać strukturę twojego kodu. Wiąże się to z kilkoma kompromisami, a mianowicie, że istnieje boski obiekt Phaser.Game, który umożliwia dostęp do wszystkiego: pamięci podręcznej, fizyki, stanów gry i innych.
Ta stan sprawia, że naprawdę trudno jest przetestować wiele funkcji, takich jak moja Tilemap. Zobaczmy przykład:
Tutaj sprawdzam, czy moje warstwy kafelków są prawidłowe, i mogę zidentyfikować ściany i stworzenia w mojej Tilemapie:
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]
]);
Niezależnie od tego, co zrobię, gdy tylko spróbuję utworzyć mapę, Phaser wewnętrznie wywołuje jej pamięć podręczną, która jest zapełniana tylko podczas działania.
Nie mogę wywołać tego testu bez załadowania całej gry.
Złożonym rozwiązaniem może być napisanie adaptera lub serwera proxy, który buduje mapę tylko wtedy, gdy musimy wyświetlić ją na ekranie. Albo mógłbym sam zapełnić grę, ręcznie ładując tylko zasoby, których potrzebuję, a następnie używając go tylko dla określonej klasy testowej lub modułu.
Wybrałem to, co uważam za bardziej pragmatyczne, ale obce rozwiązanie tego problemu. Pomiędzy ładowaniem mojej gry a faktycznym jej odtwarzaniem przeskoczyłem TestState
w, który uruchamia test ze wszystkimi zasobami i danymi z pamięci podręcznej już załadowanymi.
To jest fajne, ponieważ mogę przetestować wszystkie funkcje, których chcę, ale także odłączyć, ponieważ jest to techniczny test integracji i zastanawiam się, czy nie mogę po prostu spojrzeć na ekran i sprawdzić, czy wrogowie są wyświetlani. W rzeczywistości nie, mogli zostać błędnie zidentyfikowani jako Przedmiot (zdarzyło się to już raz) lub - później w testach - mogli nie otrzymać wydarzeń związanych z ich śmiercią.
Moje pytanie - czy shimming w stanie testowym jest taki powszechny? Czy istnieją lepsze podejścia, zwłaszcza w środowisku JavaScript, o których nie wiem?
Inny przykład:
OK, oto bardziej konkretny przykład, który pomoże wyjaśnić, co się dzieje:
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);
}
Skonstruowałem Tilemap z trzech części:
- Mapy
key
- Szczegółowy
manifest
opis wszystkich zasobów (arkuszy i arkuszy) wymaganych przez mapę - A,
mapDefinition
który opisuje strukturę i warstwy tilemapy.
Po pierwsze, muszę zadzwonić do super, aby zbudować Tilemap w Phaserze. Jest to część, która wywołuje wszystkie te połączenia do pamięci podręcznej, gdy próbuje wyszukać rzeczywiste zasoby, a nie tylko klucze zdefiniowane w pliku manifest
.
Po drugie, łączę arkusze i warstwy płytek z Tilemap. Może teraz renderować mapę.
Po trzecie, iterację moich warstw i znaleźć jakieś specjalne przedmioty, które chcę do wyciągnięcia z mapy: Creatures
, Items
, Interactables
i tak dalej. Te obiekty tworzę i przechowuję do późniejszego wykorzystania.
Obecnie nadal mam stosunkowo prosty interfejs API, który pozwala mi znajdować, usuwać i aktualizować te podmioty:
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 });
}
Właśnie tę funkcjonalność chcę sprawdzić. Jeśli nie dodam warstw lub zestawów kafelków, mapa nie będzie renderowana, ale być może będę w stanie ją przetestować. Jednak nawet wywołanie super (...) wywołuje logikę kontekstową lub stanową, której nie mogę wyodrębnić w moich testach.
new Tilemap(...)
Phaser zaczyna kopać w swojej pamięci podręcznej. Musiałbym to odłożyć, ale to oznacza, że moja Mapa Tilemap jest w dwóch stanach, jednym, który nie może się odpowiednio oddać, i w pełni zbudowanym.Odpowiedzi:
Nie znając Phasera ani Typescipt, wciąż próbuję dać ci odpowiedź, ponieważ problemy, z którymi się borykasz, są problemami widocznymi również w wielu innych frameworkach. Problem polega na tym, że komponenty są ściśle ze sobą powiązane (wszystko wskazuje na obiekt Boga, a obiekt Boga jest właścicielem wszystkiego ...). Jest to mało prawdopodobne, jeśli twórcy frameworka sami stworzą testy jednostkowe.
Zasadniczo masz cztery opcje:
Tych opcji nie należy wybierać, chyba że zawiodą wszystkie inne opcje.
Wybór innego frameworka, który korzysta z testów jednostkowych i ma utratę sprzężenia, znacznie ułatwi życie. Ale być może nie ma nic, co by ci się podobało i dlatego utknąłeś w ramach, które masz teraz. Pisanie własnego może zająć dużo czasu.
Prawdopodobnie najłatwiejszy do zrobienia, ale tak naprawdę zależy to od tego, ile czasu masz i od tego, jak chętni twórcy frameworka są w stanie zaakceptować żądania ściągnięcia.
Ta opcja jest prawdopodobnie najlepszą opcją na rozpoczęcie testów jednostkowych. Zawiń niektóre obiekty, których naprawdę potrzebujesz w testach jednostkowych, i stwórz fałszywe obiekty do końca.
źródło
Podobnie jak David, nie znam Phasera ani Maszynopisu, ale dostrzegam twoje obawy jako wspólne dla testów jednostkowych z frameworkami i bibliotekami.
Krótka odpowiedź brzmi: tak, shimming jest prawidłowym i powszechnym sposobem radzenia sobie z tym przy testowaniu jednostkowym . Myślę, że odłączenie rozumie różnicę między izolowanymi testami jednostkowymi a testami funkcjonalnymi.
Testy jednostkowe dowodzą, że małe fragmenty kodu dają prawidłowe wyniki. Celem testu jednostkowego nie jest testowanie kodu innej firmy. Zakładamy, że kod został już przetestowany pod kątem działania zgodnie z oczekiwaniami innej firmy. Podczas pisania testu jednostkowego kodu opartego na frameworku często shimuje się pewne zależności, aby przygotować kod, który wygląda jak określony stan, lub całkowicie shimuje framework / bibliotekę. Prostym przykładem jest zarządzanie sesją dla witryny internetowej: być może podkładka zawsze zwraca prawidłowy, spójny stan zamiast odczytu z pamięci. Innym częstym przykładem jest pomijanie danych w pamięci i omijanie dowolnej biblioteki, która zapytałaby bazę danych, ponieważ celem nie jest testowanie bazy danych lub biblioteki, której używasz do łączenia się z nią, a jedynie prawidłowe przetwarzanie danych w kodzie.
Ale dobre testy jednostkowe nie oznaczają, że użytkownik końcowy zobaczy dokładnie to, czego oczekujesz. Testy funkcjonalne mają bardziej ogólny widok, że działa cała funkcja, frameworki i inne. Wracając do przykładu prostej witryny, test funkcjonalny może wysłać żądanie do kodu i sprawdzić poprawność wyników. Obejmuje cały kod wymagany do uzyskania wyników. Test dotyczy funkcjonalności bardziej niż konkretnej poprawności kodu.
Myślę więc, że jesteś na dobrej drodze z testami jednostkowymi. Aby dodać testowanie funkcjonalne całego systemu, stworzyłbym osobne testy, które wywołują środowisko wykonawcze Phaser i sprawdzają wyniki.
źródło