Unit тесты

Работа с Mapbox GL JS в контексте unit-тестов требует учёта специфики WebGL, асинхронной загрузки стилей и сложной внутренней модели состояния карты. Классические подходы к тестированию DOM-логики оказываются недостаточными, поскольку значительная часть поведения библиотеки связана с графическим рендерингом и взаимодействием с GPU.

Особенности тестирования Mapbox GL JS

Основная сложность unit-тестирования связана с тем, что карта:

  • зависит от WebGL-контекста;
  • асинхронно загружает стили, тайлы и источники данных;
  • имеет внутренний event-driven цикл обновления;
  • хранит состояние рендеринга вне DOM.

Поэтому прямое создание экземпляра карты в тестовой среде без подготовки приводит к ошибкам и нестабильным тестам.

Ключевое ограничение:

Mapbox GL JS не предназначен для выполнения в Node.js без эмуляции WebGL.

Подготовка тестового окружения

Для unit-тестирования обычно используется связка:

  • Jest или Vitest
  • jsdom (для DOM)
  • headless-gl (для WebGL эмуляции)
  • мокирование сетевых запросов

Базовая настройка Jest:

module.exports = {
  testEnvironment: "jsdom",
  setupFilesAfterEnv: ["<rootDir>/test/setup.js"]
};

Эмуляция WebGL контекста

Без WebGL карта не инициализируется. Для этого применяется headless-gl:

const { createCanvas } = require("canvas");
const gl = require("gl");

HTMLCanvasElement.prototype.getContext = function (type) {
  if (type === "webgl" || type === "experimental-webgl") {
    return gl(256, 256);
  }
  return null;
};

Такой подход позволяет пройти этап инициализации рендера, но не гарантирует корректность визуального результата.

Мокирование Mapbox GL JS

В unit-тестах часто используется частичная или полная подмена API карты.

Пример минимального мок-объекта:

class MapMock {
  constructor(options) {
    this.options = options;
    this.events = {};
  }

  on(event, handler) {
    this.events[event] = handler;
  }

  off() {}

  addLayer(layer) {
    this.layer = layer;
  }

  getSource(id) {
    return { id };
  }

  remove() {}
}

module.exports = { Map: MapMock };

Такой подход позволяет тестировать бизнес-логику, не затрагивая WebGL-слой.

Тестирование инициализации карты

Unit-тесты часто проверяют корректность передачи конфигурации:

import { Map } from "mapbox-gl";

test("инициализация карты с корректными параметрами", () => {
  const map = new Map({
    container: "map",
    style: "mapbox://styles/example/style",
    center: [30, 50],
    zoom: 10
  });

  expect(map.options.zoom).toBe(10);
  expect(map.options.center).toEqual([30, 50]);
});

Здесь тестируется не рендеринг, а корректность конфигурации.

Проверка добавления слоёв

Работа со слоями является частой частью логики:

test("добавление слоя сохраняется в состоянии карты", () => {
  const map = new MapMock();

  map.addLayer({
    id: "points",
    type: "circle",
    source: "points-source"
  });

  expect(map.layer.id).toBe("points");
});

В реальном Mapbox GL JS добавление слоя инициирует пересчёт рендера, но в unit-тестах это заменяется проверкой состояния.

Тестирование источников данных (sources)

Источники данных являются фундаментальной частью архитектуры:

test("getSource возвращает источник по id", () => {
  const map = new MapMock();

  const source = map.getSource("cities");

  expect(source.id).toBe("cities");
});

При необходимости мок может расширяться для имитации GeoJSON, vector tiles и raster sources.

Обработка событий

Mapbox GL JS активно использует события:

  • load
  • render
  • click
  • move

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

test("обработчик события load вызывается", () => {
  const map = new MapMock();
  const handler = jest.fn();

  map.on("load", handler);
  map.events.load();

  expect(handler).toHaveBeenCalled();
});

Асинхронная загрузка стилей

Стиль карты загружается асинхронно, что требует имитации Promise-логики:

function loadStyle(map, style) {
  return new Promise((resolve) => {
    map.setStyle(style);
    resolve(style);
  });
}

test("загрузка стиля завершается успешно", async () => {
  const map = new MapMock();

  const style = await loadStyle(map, "dark");

  expect(style).toBe("dark");
});

Snapshot-тестирование конфигураций

Snapshot-тесты полезны для фиксации структуры конфигурации:

test("конфигурация карты соответствует ожиданию", () => {
  const config = {
    style: "streets",
    zoom: 12,
    center: [40, 55]
  };

  expect(config).toMatchSnapshot();
});

Разделение unit и integration тестов

При работе с Mapbox GL JS важно разграничивать уровни:

  • Unit-тесты: проверка логики, конфигураций, маппинга данных
  • Integration-тесты: работа с реальным WebGL и DOM
  • E2E-тесты: проверка пользовательских сценариев

Unit-тесты не должны зависеть от рендера карты или сетевых запросов.

Мокирование сетевых запросов

Загрузка стилей и тайлов часто требует подмены fetch:

global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({ version: 8 })
  })
);

Это позволяет тестировать поведение карты без обращения к внешним сервисам.

Проверка бизнес-логики поверх карты

Часто Mapbox GL JS используется как визуальный слой поверх данных. В таких случаях unit-тесты концентрируются на преобразовании данных:

function transformPoints(points) {
  return points.map(p => ({
    type: "Feature",
    geometry: {
      type: "Point",
      coordinates: [p.lng, p.lat]
    }
  }));
}

test("преобразование точек в GeoJSON", () => {
  const result = transformPoints([{ lat: 10, lng: 20 }]);

  expect(result[0].geometry.coordinates).toEqual([20, 10]);
});

Ограничения unit-тестирования Mapbox GL JS

Некоторые аспекты невозможно корректно покрыть unit-тестами:

  • реальный WebGL рендеринг
  • производительность отрисовки
  • визуальные артефакты
  • взаимодействие с GPU
  • поведение тайлов в сети

Эти области требуют интеграционного или визуального тестирования.

Подход к стабильной архитектуре тестов

Для снижения хрупкости тестов применяется стратегия абстракции:

  • отделение логики данных от карты
  • использование адаптеров над Mapbox API
  • инкапсуляция инициализации карты
  • минимизация прямых вызовов new Map()

Такой подход позволяет тестировать бизнес-логику независимо от специфики рендера и ограничений WebGL.