CesiumJS функционирует поверх WebGL и активно использует графический контекст, асинхронную загрузку тайлов, текстур и сложные сценографические структуры. Это делает классическое unit-тестирование нетривиальным: значительная часть логики завязана на окружение браузера, GPU и таймеры рендер-цикла.
Основная сложность заключается в том, что чистые функции встречаются реже, чем в типичных JS-библиотеках, а большинство объектов Cesium (Viewer, Scene, Entity, ImageryLayer) предполагают наличие графического контекста и активного рендеринга.
Модульные тесты в такой архитектуре строятся вокруг трёх принципов:
На практике применяются связки:
Для запуска в Node.js без браузера требуется эмуляция DOM и графического контекста.
import { JSDOM } from "jsdom";
const dom = new JSDOM(`<html><body></body></html>`, {
pretendToBeVisual: true,
});
global.window = dom.window;
global.document = dom.window.document;
global.navigator = dom.window.navigator;
Однако этого недостаточно, поскольку Cesium требует WebGL.
Cesium активно использует WebGLRenderingContext, поэтому применяются:
Пример базовой подмены:
import { createContext } from "gl";
global.WebGLRenderingContext = {};
global.WebGL2RenderingContext = {};
const gl = createContext(1, 1);
global.canvas = gl.canvas;
global.WebGLRenderingContext = gl;
При более сложных сценариях создаётся фабрика контекстов:
export function createMockContext() {
return {
canvas: {},
getParameter: jest.fn(),
createShader: jest.fn(),
shaderSource: jest.fn(),
compileShader: jest.fn(),
createProgram: jest.fn(),
};
}
Viewer — высокоуровневая абстракция Cesium, включающая
рендерер, камеру, слой тайлов и событийную систему.
В unit-тестах его создание часто избегается. Вместо этого тестируется
логика, которая принимает Scene или Camera как
зависимость.
export function updateCameraPosition(camera, position) {
camera.setView({
destination: position,
});
}
Тест:
test("updateCameraPosition вызывает setView", () => {
const camera = {
setView: jest.fn(),
};
updateCameraPosition(camera, [0, 0, 100]);
expect(camera.setView).toHaveBeenCalled();
});
Entity — один из ключевых слоёв абстракции Cesium.
Тестирование обычно сводится к проверке:
import { Cartesian3 } from "cesium";
function createPointEntity(position) {
return {
position,
point: {
pixelSize: 10,
},
};
}
test("создание point entity", () => {
const entity = createPointEntity(Cartesian3.fromDegrees(10, 20));
expect(entity.point.pixelSize).toBe(10);
});
Cesium использует Property систему (ConstantProperty, CallbackProperty).
Тестирование требует проверки значений во времени:
import { CallbackProperty } from "cesium";
test("CallbackProperty возвращает актуальное значение", () => {
let value = 1;
const prop = new CallbackProperty(() => value, false);
expect(prop.getValue()).toBe(1);
value = 5;
expect(prop.getValue()).toBe(5);
});
GeoJSON, CZML и другие источники требуют мокирования fetch.
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ type: "FeatureCollection", features: [] }),
})
);
import { GeoJsonDataSource } from "cesium";
test("GeoJsonDataSource загружается", async () => {
const ds = new GeoJsonDataSource();
await ds.load("mock-url");
expect(ds.entities.values.length).toBeDefined();
});
Cesium активно использует simulation time.
Для детерминированных тестов требуется фиксация времени:
import { JulianDate } from "cesium";
test("фиксированное время", () => {
const time = JulianDate.fromIso8601("2020-01-01T00:00:00Z");
expect(JulianDate.toIso8601(time)).toContain("2020");
});
При тестировании анимации часто мокируется
requestAnimationFrame.
ImageryLayer и Cesium3DTileset требуют сетевых запросов и GPU.
Типичная стратегия:
const tilesetMock = {
readyPromise: Promise.resolve(true),
update: jest.fn(),
show: true,
};
Cesium предоставляет богатый набор математических функций:
Эти модули хорошо тестируются без мокирования WebGL.
import { Cartesian3 } from "cesium";
test("нормализация вектора", () => {
const v = Cartesian3.normalize(
new Cartesian3(2, 0, 0),
new Cartesian3()
);
expect(v.x).toBe(1);
});
Ключевая архитектурная практика — минимизация логики внутри Cesium-объектов.
Хороший слой:
export function calculateDistance(a, b, cesiumMath) {
return cesiumMath.distance(a, b);
}
Cesium Event — функциональная система обратных вызовов.
test("Cesium Event вызывает callback", () => {
const event = {
listeners: [],
addEventListener(fn) {
this.listeners.push(fn);
},
raise(arg) {
this.listeners.forEach((fn) => fn(arg));
},
};
const mock = jest.fn();
event.addEventListener(mock);
event.raise(10);
expect(mock).toHaveBeenCalledWith(10);
});
Viewer и Scene нельзя запускать без GPU-окружения без значительной подмены.
Tileset и imagery могут зависеть от сети и кеша.
requestAnimationFrame создаёт нестабильность в тестах.
Entity может содержать цепочки Property, которые сложно сравнивать напрямую.
Используются следующие подходы:
function createLabel(entityApi, text) {
return entityApi.create({
label: {
text,
font: "12px sans-serif",
},
});
}
test("createLabel формирует entity", () => {
const api = {
create: jest.fn((x) => x),
};
const result = createLabel(api, "Test");
expect(result.label.text).toBe("Test");
});