Тестирование кода в приложениях, использующих Mapbox GL JS, требует разделения логики на изолированные части и строгого контроля состояния карты, источников данных и визуальных слоёв. Основная сложность заключается в том, что большая часть API работает с асинхронным рендерингом WebGL, DOM-событиями и внешними тайловыми источниками.
Ключевая стратегия — отделение бизнес-логики от слоя отображения карты. Это позволяет тестировать функции без необходимости инициализации полноценного экземпляра карты.
Функции, связанные с картой, условно делятся на три категории:
Чистые функции являются основой тестируемости. Они не зависят от
map-объекта и работают только с входными данными.
function formatPopupData(feature) {
return {
title: feature.properties.name,
coordinates: feature.geometry.coordinates,
description: feature.properties.description || ''
};
}
Такие функции легко тестируются без использования Mapbox GL JS.
Для тестирования используется стандартный стек: Jest, Vitest или Mocha.
Пример теста:
import { formatPopupData } from './formatPopupData';
test('formatPopupData корректно преобразует feature', () => {
const feature = {
properties: {
name: 'Park',
description: 'City park'
},
geometry: {
coordinates: [30, 50]
}
};
expect(formatPopupData(feature)).toEqual({
title: 'Park',
coordinates: [30, 50],
description: 'City park'
});
});
Основной принцип — не включать map и WebGL в
unit-тесты.
Функции, работающие с объектом map, сложнее тестировать,
поскольку они зависят от состояния рендера и асинхронных событий.
Пример функции:
function addCustomLayer(map, id, sourceId) {
map.addLayer({
id,
type: 'circle',
source: sourceId,
paint: {
'circle-radius': 6,
'circle-color': '#ff0000'
}
});
}
Для тестирования используется мокирование объекта
map.
Создаётся фейковый объект карты:
const mockMap = {
addLayer: jest.fn(),
removeLayer: jest.fn(),
addSource: jest.fn(),
getSource: jest.fn()
};
Тест:
test('addCustomLayer вызывает map.addLayer с корректными параметрами', () => {
addCustomLayer(mockMap, 'points-layer', 'points-source');
expect(mockMap.addLayer).toHaveBeenCalledWith({
id: 'points-layer',
type: 'circle',
source: 'points-source',
paint: {
'circle-radius': 6,
'circle-color': '#ff0000'
}
});
});
Такой подход позволяет изолировать логику от WebGL и DOM.
Mapbox GL JS активно использует событийную модель: load,
click, move, render.
Для тестирования событий используется имитация event emitter.
const mockMap = {
on: jest.fn((event, handler) => {
if (event === 'load') {
handler();
}
})
};
Пример функции:
function initMapLogic(map) {
map.on('load', () => {
map.addSource('points', {
type: 'geojson',
data: '/data/points.json'
});
});
}
Тест:
test('initMapLogic добавляет source при load', () => {
const map = {
on: jest.fn((event, cb) => event === 'load' && cb()),
addSource: jest.fn()
};
initMapLogic(map);
expect(map.addSource).toHaveBeenCalledWith('points', {
type: 'geojson',
data: '/data/points.json'
});
});
Часто функции взаимодействуют с API и GeoJSON источниками.
async function fetchGeoJSON(url) {
const response = await fetch(url);
return await response.json();
}
Тестирование выполняется через мок fetch.
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ type: 'FeatureCollection', features: [] })
})
);
Тест:
test('fetchGeoJSON возвращает данные', async () => {
const data = await fetchGeoJSON('/data.json');
expect(data).toEqual({
type: 'FeatureCollection',
features: []
});
});
Работа с addSource, setData,
removeSource требует проверки состояния.
function updateSource(map, sourceId, data) {
const source = map.getSource(sourceId);
source.setData(data);
}
Тест:
test('updateSource обновляет данные источника', () => {
const setData = jest.fn();
const map = {
getSource: jest.fn(() => ({
setData
}))
};
updateSource(map, 'points', { type: 'FeatureCollection', features: [] });
expect(setData).toHaveBeenCalledWith({
type: 'FeatureCollection',
features: []
});
});
Интеграционные тесты проверяют взаимодействие нескольких компонентов: источники, слои, события.
Обычно применяется headless-окружение с ограниченным рендерингом.
Структура теста:
Основные сложности:
Решения:
jest-canvas-mockКонтролы реализуются как объекты с методами onAdd и
onRemove.
class CustomControl {
onAdd(map) {
this.map = map;
this.container = document.createElement('div');
this.container.textContent = 'Control';
return this.container;
}
onRemove() {
this.container.remove();
this.map = undefined;
}
}
Тест:
test('CustomControl создаёт DOM элемент', () => {
const control = new CustomControl();
global.document = {
createElement: jest.fn(() => ({
textContent: '',
remove: jest.fn()
}))
};
const container = control.onAdd({});
expect(container.textContent).toBe('Control');
});
Для устойчивых тестов в проектах с Mapbox GL JS применяется следующая структура:
Карты Mapbox GL JS содержат внутреннее состояние, которое сложно контролировать напрямую. Поэтому часто используется паттерн обёртки:
class MapService {
constructor(map) {
this.map = map;
}
addPointLayer() {
this.map.addLayer({
id: 'points',
type: 'circle',
source: 'points-source'
});
}
}
Тестируется уже MapService, а не сам
map.
test('MapService добавляет слой', () => {
const addLayer = jest.fn();
const service = new MapService({
addLayer
});
service.addPointLayer();
expect(addLayer).toHaveBeenCalled();
});