Unit тесты

CesiumJS функционирует поверх WebGL и активно использует графический контекст, асинхронную загрузку тайлов, текстур и сложные сценографические структуры. Это делает классическое unit-тестирование нетривиальным: значительная часть логики завязана на окружение браузера, GPU и таймеры рендер-цикла.

Основная сложность заключается в том, что чистые функции встречаются реже, чем в типичных JS-библиотеках, а большинство объектов Cesium (Viewer, Scene, Entity, ImageryLayer) предполагают наличие графического контекста и активного рендеринга.

Модульные тесты в такой архитектуре строятся вокруг трёх принципов:

  • изоляция логики от WebGL
  • подмена (mock) графических и сетевых зависимостей
  • контроль детерминизма времени и асинхронных операций

Тестовая инфраструктура

На практике применяются связки:

  • Jest
  • Vitest
  • Mocha + Chai

Для запуска в Node.js без браузера требуется эмуляция DOM и графического контекста.

Базовое окружение Node.js

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.


Эмуляция WebGL

Cesium активно использует WebGLRenderingContext, поэтому применяются:

  • headless-gl
  • webgl-mock
  • context mocking через jest

Пример базовой подмены:

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 и Scene

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-модели

Entity — один из ключевых слоёв абстракции Cesium.

Тестирование обычно сводится к проверке:

  • корректности создания свойств
  • реактивных Property
  • преобразований координат

Пример проверки свойства

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);
});

Property и реактивность

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);
});

DataSource и загрузка данных

GeoJSON, CZML и другие источники требуют мокирования fetch.

Мок fetch

global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({ type: "FeatureCollection", features: [] }),
  })
);

Тест DataSource

import { GeoJsonDataSource } from "cesium";

test("GeoJsonDataSource загружается", async () => {
  const ds = new GeoJsonDataSource();

  await ds.load("mock-url");

  expect(ds.entities.values.length).toBeDefined();
});

Работа с таймингом и Clock

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.

Типичная стратегия:

  • замена загрузчиков
  • отключение реального рендеринга
  • подмена readyPromise
const tilesetMock = {
  readyPromise: Promise.resolve(true),
  update: jest.fn(),
  show: true,
};

Геометрия и математические утилиты

Cesium предоставляет богатый набор математических функций:

  • Cartesian3
  • Matrix4
  • Quaternion
  • Ellipsoid

Эти модули хорошо тестируются без мокирования WebGL.

import { Cartesian3 } from "cesium";

test("нормализация вектора", () => {
  const v = Cartesian3.normalize(
    new Cartesian3(2, 0, 0),
    new Cartesian3()
  );

  expect(v.x).toBe(1);
});

Разделение логики и Cesium API

Ключевая архитектурная практика — минимизация логики внутри Cesium-объектов.

Хороший слой:

  • чистые функции (геодезия, расчёты)
  • адаптеры Cesium
  • UI слой отдельно
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);
});

Частые проблемы unit-тестирования Cesium

Жёсткая привязка к WebGL

Viewer и Scene нельзя запускать без GPU-окружения без значительной подмены.

Неконтролируемый async

Tileset и imagery могут зависеть от сети и кеша.

Недетерминированный render loop

requestAnimationFrame создаёт нестабильность в тестах.

Сложность объектов

Entity может содержать цепочки Property, которые сложно сравнивать напрямую.


Стратегия стабилизации тестов

Используются следующие подходы:

  • мокирование Scene primitives
  • отключение render loop
  • подмена clock step
  • фиксация Promise.resolve цепочек
  • изоляция Cesium API через адаптеры

Пример изолированного unit-теста слоя приложения

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");
});