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

Тестирование кода в приложениях, использующих Mapbox GL JS, требует разделения логики на изолированные части и строгого контроля состояния карты, источников данных и визуальных слоёв. Основная сложность заключается в том, что большая часть API работает с асинхронным рендерингом WebGL, DOM-событиями и внешними тайловыми источниками.

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


Разделение логики для тестирования

Функции, связанные с картой, условно делятся на три категории:

  • чистые функции (pure functions)
  • функции работы с состоянием карты
  • асинхронные функции загрузки данных и источников

Чистые функции являются основой тестируемости. Они не зависят от map-объекта и работают только с входными данными.

function formatPopupData(feature) {
    return {
        title: feature.properties.name,
        coordinates: feature.geometry.coordinates,
        description: feature.properties.description || ''
    };
}

Такие функции легко тестируются без использования Mapbox GL JS.


Unit-тестирование чистых функций

Для тестирования используется стандартный стек: 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.


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

Создаётся фейковый объект карты:

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-окружение с ограниченным рендерингом.

Структура теста:

  • инициализация карты
  • добавление источников
  • добавление слоёв
  • проверка состояния через API карты

Проблемы тестирования WebGL

Основные сложности:

  • отсутствие DOM в тестовой среде
  • невозможность рендеринга WebGL
  • асинхронная загрузка стилей и тайлов
  • нестабильность событий render

Решения:

  • использование моков Mapbox GL JS
  • отключение рендеринга через 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 применяется следующая структура:

  • логика работы с данными — чистые функции
  • работа с картой — через интерфейсы и моки
  • API-запросы — через отдельный слой сервисов
  • UI-контролы — через DOM-моки
  • интеграционные тесты — минимально необходимые сценарии

Изоляция состояния карты

Карты 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();
});