Mocking провайдеров

В тестировании и разработке приложений на CesiumJS ключевым становится контроль над внешними источниками данных: тайловыми изображениями, рельефом, 3D Tiles и сервисами Cesium Ion. Эти зависимости делают систему детерминированной только при условии изоляции, поэтому мокирование провайдеров превращается в базовый инструмент устойчивой архитектуры.

CesiumJS строится вокруг абстракций поставщиков данных. Основные интерфейсы:

  • ImageryProvider — источники тайловых изображений
  • TerrainProvider — модели рельефа
  • Cesium3DTileset — потоковые 3D Tiles наборы
  • DataSource — сущности сцены
  • Resource — низкоуровневые HTTP-запросы

Каждый из этих компонентов предполагает сетевую загрузку и асинхронную работу, что делает прямое тестирование без подмены источников нестабильным.

Mocking позволяет заменить реальные сервисы предсказуемыми реализациями с фиксированным поведением.

Принцип подмены через полиморфизм

CesiumJS активно использует интерфейсную совместимость: большинство провайдеров не требуют наследования от базового класса, достаточно реализовать ожидаемые методы.

Ключевые точки подмены:

  • getTileCredits
  • requestImage
  • requestTileGeometry
  • sampleTerrainMostDetailed
  • isReady
  • load / unload (для tilesets)

Это позволяет создавать легковесные реализации без подключения сети и без Cesium Ion.

Mock ImageryProvider

Самый частый сценарий — замена тайлового слоя на статическое изображение или процедурную генерацию.

class MockImageryProvider {
  constructor() {
    this.tileWidth = 256;
    this.tileHeight = 256;
    this.maximumLevel = 1;
    this.minimumLevel = 0;
    this.tilingScheme = new Cesium.WebMercatorTilingScheme();
    this.ready = true;
  }

  requestImage(x, y, level) {
    const canvas = document.createElement("canvas");
    canvas.width = 256;
    canvas.height = 256;

    const ctx = canvas.getContext("2d");

    ctx.fillStyle = level % 2 === 0 ? "#2b2b2b" : "#3a3a3a";
    ctx.fillRect(0, 0, 256, 256);

    ctx.fillStyle = "#ffffff";
    ctx.fillText(`L${level} X${x} Y${y}`, 20, 130);

    return Promise.resolve(canvas);
  }

  getTileCredits() {
    return [];
  }
}

Такой провайдер полностью исключает сетевые задержки и позволяет стабилизировать тесты визуализации слоёв.

Mock TerrainProvider

Рельеф является более чувствительным компонентом, поскольку влияет на позиционирование камер и высотные вычисления. Упрощённый TerrainProvider обычно возвращает нулевую высоту.

class FlatTerrainProvider {
  constructor() {
    this.ellipsoid = Cesium.Ellipsoid.WGS84;
    this.ready = true;
    this.hasWaterMask = false;
    this.hasVertexNormals = false;
  }

  isReady() {
    return true;
  }

  requestTileGeometry(x, y, level) {
    return Promise.resolve({
      buffer: new Uint8Array(),
      encoding: undefined,
      stride: 0,
      vertexCount: 0
    });
  }

  getLevelMaximumGeometricError(level) {
    return 1.0;
  }
}

На практике более корректный вариант — делегирование в Cesium.EllipsoidTerrainProvider с отключённой детализацией, но полностью моковый вариант используется в unit-тестах.

Мокирование Cesium3DTileset

3D Tiles является наиболее сложным объектом для подмены, поскольку включает:

  • загрузку батчей
  • геометрические иерархии
  • стриминг LOD

Полная эмуляция невозможна без значительного объёма кода, поэтому используется упрощённый заглушечный tileset.

class MockTileset {
  constructor() {
    this.readyPromise = Promise.resolve(true);
    this._ready = true;
    this.show = true;
    this.modelMatrix = Cesium.Matrix4.IDENTITY;
  }

  update() {
    // имитация кадра обновления без загрузки данных
  }

  traverse(callback) {
    // пустой обход дерева
  }

  destroy() {
    this._ready = false;
  }
}

Такая реализация позволяет тестировать логику интеграции Viewer без реальной загрузки ассетов.

Инъекция зависимостей через Viewer

Cesium Viewer принимает провайдеры в момент создания, что делает возможной подмену без изменения внутренней логики сцены.

const viewer = new Cesium.Viewer("container", {
  imageryProvider: new MockImageryProvider(),
  terrainProvider: new FlatTerrainProvider(),
  baseLayerPicker: false
});

Такой подход стабилизирует:

  • unit-тесты UI
  • snapshot-тестирование сцены
  • headless-рендеринг

Перехват сетевых запросов

Мокирование провайдеров часто дополняется перехватом fetch-запросов, поскольку Cesium Resource использует стандартный HTTP слой.

Сценарии:

  • подмена Cesium Ion endpoints
  • эмуляция 404/timeout
  • фиксация геоданных

Пример с переопределением fetch:

global.fetch = (url) => {
  if (url.includes("terrain")) {
    return Promise.resolve({
      ok: true,
      arrayBuffer: () => Promise.resolve(new ArrayBuffer(0))
    });
  }

  return Promise.reject(new Error("Unknown resource"));
};

Более контролируемый вариант — использование MSW (Mock Service Worker), позволяющего фильтровать запросы на уровне сети без изменения кода приложения.

Мокирование Cesium Resource

Resource является центральной абстракцией HTTP в CesiumJS. Подмена его методов позволяет изолировать любые сетевые операции.

Cesium.Resource.fetchArrayBuffer = () => {
  return Promise.resolve(new ArrayBuffer(8));
};

Cesium.Resource.fetchJson = () => {
  return Promise.resolve({ mocked: true });
};

Такая стратегия особенно эффективна при тестировании DataSource и CZML.

Поведение времени и Clock mocking

Cesium Viewer сильно зависит от симуляции времени. Мокирование Clock позволяет фиксировать анимации и воспроизводимость кадров.

const clock = new Cesium.Clock({
  startTime: Cesium.JulianDate.now(),
  currentTime: Cesium.JulianDate.now(),
  multiplier: 1,
  shouldAnimate: true
});

clock.tick = () => {
  return Cesium.JulianDate.addSeconds(
    clock.currentTime,
    1,
    new Cesium.JulianDate()
  );
};

Фиксированное время устраняет недетерминированность при тестировании динамических сцен.

Изоляция Cesium Ion

Cesium Ion часто становится источником внешней зависимости. Подмена токенов и endpoint позволяет полностью отключить облачную часть:

Cesium.Ion.defaultAccessToken = "mock-token";

Cesium.IonResource._request = () => {
  return Promise.resolve({
    data: {},
    statusCode: 200
  });
};

В связке с моками tileset это полностью исключает необходимость сетевого доступа.

Стратегии уровней мокирования

В зависимости от цели тестирования выделяются уровни:

  • Unit mocking Подмена отдельных методов (fetchImage, sampleTerrain)

  • Integration mocking Замена провайдеров целиком (ImageryProvider, TerrainProvider)

  • Scene mocking Подмена Viewer конфигурации

  • Network mocking Перехват HTTP слоя

Комбинация уровней позволяет балансировать между скоростью тестов и реалистичностью сцены.

Проблемы согласованности моков

Неполная имитация провайдера приводит к расхождениям:

  • несовпадение координатных систем
  • некорректные tile bounds
  • ошибки LOD traversal
  • различия в promise resolution timing

Поэтому мок должен сохранять контракт Cesium API, а не только форму данных.

Использование процедурной генерации вместо моков

В некоторых случаях вместо заглушек применяются процедурные провайдеры:

  • шум Перлина для рельефа
  • градиентные текстуры для imagery
  • синтетические tileset-и

Это позволяет сохранять визуальную структуру сцены без сетевых зависимостей и при этом избегать пустых экранов.

Контроль асинхронности

Cesium активно использует Promise-based pipeline, поэтому мокирование требует контроля микротасков:

await Promise.resolve(); // стабилизация кадра

Без синхронизации тесты становятся нестабильными из-за race conditions между render loop и загрузкой ресурсов.